What does the interfaces in GraphQLInterfaceType? - javascript

I have the following the code snippet:
export const NodeInterface = new GraphQLInterfaceType({
name: 'Node',
fields: {
id: {
type: new GraphQLNonNull(GraphQLID)
}
},
resolveType: (source) => {
if (source.__tableName === tables.users.getName()) {
return UserType;
}
return PostType;
}
});
and a GraphQLObjectType that is using the interface:
export const PostType = new GraphQLObjectType({
name: 'Post',
interfaces: [ NodeInterface ],
fields: {
id: {
type: new GraphQLNonNull(GraphQLID),
resolve: resolveId
},
createdAt: {
type: new GraphQLNonNull(GraphQLString),
},
body: {
type: new GraphQLNonNull(GraphQLString)
}
}
});
For what do I have to define an interface?

In GraphQL, interfaces fulfill two purposes:
They ensure that the types implementing them also implement specific fields. For example, the Node interface here has an id field -- that means any type that implements the Node interface will also need to have an id field (and that id will need to be an ID scalar like in the interface) The same goes for arguments on those -- any arguments on the fields in the interface will also have to exist on the matching fields in the implementing type.
They can be used when two or more types are expected for a field. A field will always resolve to exactly one type or scalar, however, by using interfaces (or unions) we indicate in our schema that the field could resolve to one of a set of types.
So let's say we have a Node like in your snippet, some types that implement it, and a query that returns a Node:
interface Node {
id: ID!
}
type Foo implements Node {
id: ID!
someFooField: String!
someOtherFooField: Int!
}
type Bar implements Node {
id: ID!
someBarField: String!
someOtherFooField: Int!
}
type Query {
getNode(id: ID!): Node!
}
In our example, getNode could resolve to either a Foo or a Bar. When we write our query, we don't know which one will be resolved. But because we know the id field is required by the interface, we can write a query like this:
query OperationName {
getNode(id: "SOME_ID"){
id
}
}
If we need to query someBarField as well, though, we can't do this:
query OperationName {
getNode(id: "SOME_ID"){
id
someBarField
}
}
because Foo doesn't have that field. Instead we have to utilize a fragment, like this:
query OperationName {
getNode(id: "SOME_ID"){
id
... on Bar {
someBarField
}
}
}
Then someBarField will be returned, but only if the field resolves to the type Bar. If it's a Foo, only id will be returned. Similarly, you can request non-shared fields from any type implementing the same interface:
query OperationName {
getNode(id: "SOME_ID"){
id
... on Bar {
someBarField
}
... on Foo {
someFooField
}
}
}
Last but not least, it should be mentioned that unions work in a very similar fashion. However, unlike interfaces, there are no shared fields defined for a Union, so a type does not "implement" a union, it just is part of one. That means when requesting a field that returns a union, you'll always have to use fragments since there are no shared fields to request.

Related

AJV does not throw type errors when given incorrect schema for a Union type in TypeScript

I have a pretty large interface, FooBase, as well as some other interfaces FooOne and FooTwo that extend FooBase but have a few fields set to specific constant values and have a few additional fields.
enum EnumField {
ONE = 'ONE',
TWO = 'TWO'
}
interface FooBase {
bar: string;
baz: string;
// many other fields
}
interface FooOne extends FooBase {
enumField: EnumField.ONE
extraField: string
}
interface FooTwo extends FooBase {
enumField: EnumField.TWO
}
type Foo = FooOne | FooTwo;
I want to use ajv and JSONSchema to construct a validator for Foo. I want ajv to tell me when my schema does not match the type. For a straightforward non-union interface, ajv will tell me if I have properties missing or have an incomplete or incorrect type. However, with the union, it seems like ajv is getting tripped up:
const FooSchema: ajv.JSONSchemaType<Foo> = {
type: 'object',
properties: {
bar: {
type: 'string'
},
baz: {
type: 'string'
},
enumField: {
type: 'string' // underspecified, can't be any string
}
},
required: []
}
I would expect the above to fail because the enumField is underspecified and there is no mentioned made of the extraField on FooOne. However, it compiles without issue. In fact, I can make this worse by giving the wrong type for extraField, and it will still compile:
const FooSchemaThatsDefinitelyWrong: ajv.JSONSchemaType<Foo> = {
type: 'object',
properties: {
bar: {
type: 'string'
},
baz: {
type: 'string'
},
enumField: {
type: 'string'
},
extraField: {
type: 'number' // should be string, if anything
}
},
required: []
}
All of this can also be found in the playground.
I would have expected ajv to tell me to use a oneOf to resolve this, but instead it just ignores any issues. What's going on here? Is this just a fundamental limitation of ajv?
It looks like it is an ajv limitation.
It says in the documentation that "utility types JSONSchemaType [...] to convert data type into the schema type to simplify writing schemas, [...] for JSON Schema (but without union support)."

typescript extending types vs interfaces

This might a be relatively noob question,
I have an interface
interface Employee {
name: string
}
and I would like to have an extended version of this after it being saved into the DB:
interface EmployeeDb {
id: string,
name: string
}
I would like to differentiate it when handling checks so after saving data in my storage, the type checker won't complain about not having id value. Meaning I want to avoid using this:
interface Employee {
id?: string,
name: string
}
so I don't have to check for id everywhere.
So I am trying to do it this way:
type Employee = {
name: string
}
type IDatabaseObject<T> = {
id: IDatabaseObjectId;
[P in keyof T]: T[P];
};
type EmployeeDb = IDatabaseObject<Employee>
which the IDE gives an error with the top syntax
A computed property name must be of type 'string', 'number', 'symbol',
or 'any'.ts(2464)
so I tried to use interface and extend it
interface IDatabaseObject {
id: string
}
interface EmployeeDb extends Employee, IDatabaseObject {}
but in the backend code when I try to use this setup I get an error from vscode eslint again. I have a small code here that adds the data to localstorage, generates a id and returns the data. see code:
class DbAsyncStorageTemplate<
InputDataType,
OutputDataType extends IDatabaseObject
> {
async addEntry(object: InputDataType): Promise<OutputDataType> {
const id: string = generateUuid()
const dbObject = { id, ...object }
dbObject.id = id
// add the item to AsyncStorage directly
await AsyncStorage.setItem(id, JSON.stringify(object))
// ERROR HERE: return the new object
return dbObject as OutputDataType
}
}
}
but I get an error from the IDE (eslint) for the last line
Conversion of type '{ id: string; } & InputDataType' to type
'OutputDataType' may be a mistake because neither type sufficiently
overlaps with the other. If this was intentional, convert the
expression to 'unknown' first. '{ id: string; } & InputDataType' is
assignable to the constraint of type 'OutputDataType', but
'OutputDataType' could be instantiated with a different subtype of
constraint 'any'.
any recommendation on how to do this properly?
I believe you're looking for intersections of types.
type Employee = {
name: string
}
type EmployeeDb = {
id: string;
} & Employee;
You could also define the raw DB interface and use Pick or Omit utilities as needed.
Pick Utility
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
I think you are looking for this: https://www.typescriptlang.org/docs/handbook/advanced-types.html#:~:text=an%20intersection%20type%3A-,//%20Use%20this%3A,%7D,-Try
You are trying to create a new type (IDatabaseObject) based on an old type (Employee, for instance; or T, in the generic case). This is a Mapped Type.
In your code,
[P in keyof T]: T[P]
returns your old type rather than members of that old type. So you need to close the curly brackets and intersect it with any other new members you want to add.
i.e. do the following for IDatabseObject
type IDatabaseObject<T> = {
id: number;
} & {
[P in keyof T]: T[P];
};

Using Mongoose and Typescript, what type should be used for a ref field of an interface?

I am using mongoose and Typescript, and I am wanting to know what type, or types, I should be using for a reference field, when creating an interface? Consider the following two related interfaces:
interface ICat {
name: string,
colour: string,
}
interface ICatDB extends ICat, Document {};
interface IMouse {
name: string,
colour: string,
chasedBy: /* ... */
}
interface IMouseDB extends IMouse, Document {};
And the schemas and models that use them:
let cat = new Schema({
name: String,
colour: String,
});
mongoose.model<ICatDB>('Cat', cat);
let mouse = new Schema({
name: String,
colour: String,
chasedBy: { type: Schema.Types.ObjectId, ref: 'Cat' }
});
mongoose.model<IMouseDB>('Mouse', mouse);
For the chasedBy field we need to consider that it can take values in three forms:
String or ObjectId, when passed to a create() method
ObjectId when returned from Mongoose
Instance of ICat when returned from Mongoose, using populate()
Is there a way that we can specify the types the interface can support, without having to resort to using any?
BTW we separated IMouse and IMouseDB, since Typescript wanted all the fields for Document filled out every time we created a new IMouse object, so this was a work around.
At least for version 5.x you could use this:
https://mongoosejs.com/docs/5.x/docs/typescript/populate.html
import { PopulatedDoc } from 'mongoose';
import ICat from './ICat';
interface IMouse {
name: string,
colour: string,
chasedBy: PopulatedDoc<ICat>
}
The docs in 6.x shows different approach:
https://mongoosejs.com/docs/typescript/populate.html
like this:
MouseModel.findOne({}).populate<{chasedBy: ICat}>('chasedBy');
Lacking any alternative answer, I went with:
import { ObjectID } from 'mongodb';
import ICat from './ICat';
interface IMouse {
name: string,
colour: string,
chasedBy: string | ObjectID | ICat
}
This describes the type variants for the 'chasedBy' property. As to whether this is the recommended approach, I can't say.
Mongoose 6.x
Mongoose offers type declarations in mongoose.Types
interface Mouse { // important: don't use I in front of the name and don't extend Document
chasedBy: mongoose.Types.ObjectId
}

How can I pass an array of objects into Query?

I'm trying to figure out how to pass an array of objects into my GraphQL query, however i'm finding the documentation a little unclear on how to do so. I'm working with Apollo in the FE, Graphql-yoga in the BE and using Prisma as my database along with their API.
Here is my query with the array of objects hard coded:
const USERS = gql`
query USERS(
$userId: ID
) {
users(
where: {
id_not: $userId
hasProducts_some: {
OR: [
{ itemId: 1 },
{ itemId: 2 }
]
}
}
) {
firstName
}
}
`;
The above query returns me what I want, where i'm a bit stuck is how to get this array:
[
{ itemId: 1 },
{ itemId: 2 }
]
passed in as a variable of the query. From what I could find online, I might need to create a GraphQLObjectType on the client side to be able to pass in an object definition. Here was my implementation of that:
import { GraphQLObjectType, GraphQLString } from 'graphql';
const ProductName = new GraphQLObjectType({
name: 'ProductName',
fields: () => ({
itemId: {
type: GraphQLString,
},
})
});
const USERS = gql`
query USERS(
$userId: ID,
$hasProducts: [ProductName]
) {
users(
where: {
id_not: $userId
hasProducts_some: {
OR: $hasProducts
}
}
) {
firstName
}
}
`;
The above returns me the following error:
Unknown type "ProductName"
Have I gone with the correct approach here for passing in arrays of objects, if so what's wrong with my implementation?
Types are created and used in creating your schema server-side. Once created, the schema cannot be modified at runtime -- it has whatever types and directives were specified when it was created. In other words, defining a new type on the client-side is meaningless -- it can't be used in any queries you send to the server since the server is not aware of the type.
If a variable (like $hasProducts) is passed to an argument (like hasProducts_some), that variable's type must match the type of the argument. This type could be a scalar (like String or Int) or it could be an input object type. What exact type that that is depends on the schema itself. To determine the type to use, you can open up your schema's documentation in GraphQL Playground (or GraphiQL) and search for the field in question (in this case, hasProducts_some).
Note that you can also just pass a single variable in for the whole where field.
Since the gql function expects a template literal, you should escape the product object like so:
const USERS = gql`
query USERS(
$userId: ID,
$hasProducts: [${ProductName}]
) {
users(
where: {
id_not: $userId
hasProducts_some: {
OR: $hasProducts
}
}
) {
firstName
}
}
`;
New to graphql. But was wondering if this can resolve it.
const USERS = gql`
query USERS(
$userId: ID,
$hasProducts: GraphQLList(ProductName)
) {
users(
where: {
id_not: $userId
hasProducts_some: {
OR: $hasProducts
}
}
) {
firstName
}
}
`;
Minor change, but am not privileged to comment . So posting it as answer.

Covariant Flow types generated by Relay mutation vs non-covariant in Queries

I am having some issues with some types generated by the compiler, specifically between a query and a mutation result that eventually result in the same type.
One is sort of like:
fragment Foo on MyType {
createdAt
hidden
locked
}
query {
foo(...) {
id
...Foo
}
}
And that generates covariant properties:
export type Foo = {|
+createdAt: any,
+hidden: boolean,
+locked: boolean
|};
And in another place I have:
mutation doSomething(...) {
clientMutationId
foo {
createdAt
hidden
locked
}
}
Which will generate the types:
export type DoSomething_foo = {
createdAt: any
hidden: boolean
locked: boolean
}
So when I'd try to match the result foo of the mutation and that query fragment calling commit, I get the error:
object type. Covariant property platform incompatible with invariant
use in
Try to force-cast your variable :
const a = { ... } : MyForceCast
Sometimes, Flow is not able to choose the right type when you mix them.

Categories