Giter Site home page Giter Site logo

graphql-compose / graphql-compose Goto Github PK

View Code? Open in Web Editor NEW
1.2K 22.0 77.0 5.6 MB

Toolkit for generating complex GraphQL Schemas on Node.js

Home Page: https://graphql-compose.github.io/

License: MIT License

JavaScript 0.23% TypeScript 99.77%
graphql schema toolkit schema-builder graphql-compose hacktoberfest

graphql-compose's Introduction

graphql-compose

codecov coverage Travis npm Commitizen friendly TypeScript compatible Backers on Open Collective Sponsors on Open Collective

graphql-compose – provides a type registry with a bunch of methods for programmatic schema construction. It allows not only to extend types but also remove fields, interfaces, args. If you want to write your graphql schema generator – graphql-compose is a good instrument for you.

  • provides methods for editing GraphQL output/input types (add/remove fields/args/interfaces)
  • introduces Resolver's – the named graphql fieldConfigs, which can be used for finding, updating, removing records
  • provides an easy way for creating relations between types via Resolver's
  • provides converter from OutputType to InputType
  • provides projection parser from AST
  • provides GraphQL schema language for defining simple types
  • adds additional types Date, Json

And a little bit more

graphql-compose-[plugin] – are declarative generators/plugins built on top of graphql-compose, which take some ORMs, schema definitions and create GraphQL Models from them or modify existing GraphQL Types.

Type generators built on top graphql-compose

Utility plugins:

Documentation

graphql-compose.github.io

Live Demos

Examples

Please follow Quick Start Guide for the complete example.

Here is just a demo of ambiguity ways of types definitions:

import { schemaComposer} from 'graphql-compose';

// You may use SDL format for type definition
const CityTC = schemaComposer.createObjectTC(`
  type City {
    code: String!
    name: String!
    population: Number
    countryCode: String
    tz: String
  }
`);

// Define type via Config object
const CountryTC = schemaComposer.createObjectTC({
  name: 'Country',
  fields: {
    title: 'String',
    geo: `type LonLat { lon: Float, lat: Float }`,
    hoisting: {
      type: () => AnotherTC,
      description: `
        You may wrap type in thunk for solving
        hoisting problems when two types cross reference
        each other.
      `,
    }
  }
});

// Or via declarative methods define some additional fields
CityTC.addFields({
  country: CountryTC, // some another Type
  ucName: { // standard GraphQL like field definition
    type: GraphQLString,
    resolve: (source) => source.name.toUpperCase(),
  },
  currentLocalTime: { // extended GraphQL Compose field definition
    type: 'Date',
    resolve: (source) => moment().tz(source.tz).format(),
    projection: { tz: true }, // load `tz` from database, when requested only `localTime` field
  },
  counter: 'Int', // shortening for only type definition for field
  complex: `type ComplexType {
    subField1: String
    subField2: Float
    subField3: Boolean
    subField4: ID
    subField5: JSON
    subField6: Date
  }`,
  list0: {
    type: '[String]',
    description: 'Array of strings',
  },
  list1: '[String]',
  list2: ['String'],
  list3: [new GraphQLOutputType(...)],
  list4: [`type Complex2Type { f1: Float, f2: Int }`],
});

// Add resolver method
CityTC.addResolver({
  kind: 'query',
  name: 'findMany',
  args: {
    filter: `input CityFilterInput {
      code: String!
    }`,
    limit: {
      type: 'Int',
      defaultValue: 20,
    },
    skip: 'Int',
    // ... other args if needed
  },
  type: [CityTC], // array of cities
  resolve: async ({ args, context }) => {
    return context.someCityDB
      .findMany(args.filter)
      .limit(args.limit)
      .skip(args.skip);
  },
});

// Remove `tz` field from schema
CityTC.removeField('tz');

// Add description to field
CityTC.extendField('name', {
  description: 'City name',
});

schemaComposer.Query.addFields({
  cities: CityTC.getResolver('findMany'),
  currentTime: {
    type: 'Date',
    resolve: () => Date.now(),
  },
});

schemaComposer.Mutation.addFields({
  createCity: CityTC.getResolver('createOne'),
  updateCity: CityTC.getResolver('updateById'),
  ...adminAccess({
    removeCity: CityTC.getResolver('removeById'),
  }),
});

function adminAccess(resolvers) {
  Object.keys(resolvers).forEach(k => {
    resolvers[k] = resolvers[k].wrapResolve(next => rp => {
      // rp = resolveParams = { source, args, context, info }
      if (!rp.context.isAdmin) {
        throw new Error('You should be admin, to have access to this action.');
      }
      return next(rp);
    });
  });
  return resolvers;
}

// construct schema which can be passed to express-graphql, apollo-server or graphql-yoga
export const schema = schemaComposer.buildSchema();

Contributors

This project exists thanks to all the people who contribute.

Backers

Thank you to all our backers! 🙏 [Become a backer]

Sponsors

Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor]

License

MIT

graphql-compose's People

Contributors

0biwankenobi avatar antoniopresto avatar ardatan avatar cbrunnkvist avatar dependabot[bot] avatar freiksenet avatar gomesalexandre avatar greenkeeper[bot] avatar greenkeeperio-bot avatar jmcmichael avatar josewhitetower avatar katopz avatar mernxl avatar micheljung avatar monkeywithacupcake avatar natac13 avatar nodkz avatar oluwatemilorun avatar pieh avatar retyui avatar rhengles avatar riginoommen avatar sensone avatar st0ffern avatar stefanprobst avatar tonyhallett avatar toverux avatar vladar avatar yoadsn avatar zapkub avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

graphql-compose's Issues

Buffer input type for uploading images / multi-part

How can I create an GraphQL InputType that uploads images (via firebase or aws)

const ImageSchema = new Schame({
  fileName: String,
});

const Image = mongoose.Schema('Image', ImageSchema);

const ImageTC = composeWithRelay(composeWithMongoose));

How do I add an input field which would be a buffer and upload the image, the image then get's sent to firebase/aws and the string is stored into the datastore.

Thanks!

addFilterArg with a Boolean value

Tried to create a filter with an $exists operator. Used the following:

tc.get('$findMany')
.wrapCloneArg('filter', 'FilterFindManyWithExists')
.addFilterArg({
  name: `externalUrlExists`,
  type: 'Boolean',
  query: (query, value, resolveParams) => {
    query.externalUrl = { $exists: value }
  }
});

This would not work when the query filters by a false value like so:

query {
 entities(filter: { externalUrlExists: false }) {
   someField
  }
}

The reason is due to how the resolve is wrapped here

The existence of the value is checked with a simple boolean test which prevents a boolean false to pass as a value.

Before I jump into trying to fix this - Is there any reason the condition would not be

if (typeof value === 'undefined')

?

Btw, a workaround until we improve this is to use a String type for example:

tc.get('$findMany')
.wrapCloneArg('filter', 'FilterFindManyWithExists')
.addFilterArg({
  name: `externalUrlExists`,
  type: 'String',
  query: (query, value, resolveParams) => {
    query.externalUrl = { $exists: value != 'false' }
  }
});

And pass "true" and "false" in the query filter. 💩

Generate types with subdocuments

I Have the following mongoose models:

// Country 
const CountrySchema = new mongoose.Schema({
code:           String,
name:           String,
dictionary:     [ { _id: false, value: String } ],
language:       [ { _id: false, code: String, name: String } ],
loc:            { type: { type: String }, geometries: [ GeometrySchema ] },
creationDate:   Date,
modifiedDate:   Date
});
const Country = mongoose.model('Country', CountrySchema);

const AddressSchema = new mongoose.Schema({
    _id:        false, 
    timezone:   String, 
    postalCode: String, 
    road:       String, 
    district:   String, 
    town:       String, 
    state:      String, 
    country_id: { type: Schema.Types.ObjectId, ref: 'Country' } 
});

// dmc
const DmcSchema = new mongoose.Schema({
    meID:        String,
    name:           String,
    address:        AddressSchema,
    country_ids:    [ { _id: false, type: Schema.Types.ObjectId, ref: 'Country' } ],
    language:       [ { _id: false, code: String, name: String, description: String } ],
    creationDate:   Date,
    modifiedDate:   Date
});
const Dmc = mongoose.model('Dmc', DmcSchema);

What I need is to create the relation between Dmc.address.country_id and Country, and the relation between Dmc.country_ids and Country.

I am stuck with addRelation, any help will be apreciate.

[Question] Master detail like relay example

Working with the schema defined in #2 and with the following defined:

export default class extends Relay.Route {
  static queries = {
    viewer: () => Relay.QL`
      query {
        viewer 
      }
    `
  };

In app.js

ReactDOM.render(
  <Relay.Renderer
    environment={Relay.Store}
    Container={App}
    queryConfig={new AppHomeRoute()}
  />,
  document.getElementById('root')
);

In components/App.js

class App extends React.Component{
   let list = this.props.viewer.dmcConnection.edges.map((edge, idx) => 
      <li key={idx}>{edge.node.name}</li>
    );
    render(){
        return(
        <div>
            <ul>
                {list}
            </ul>
        </div>
        );
    }
}

export default Relay.createContainer(App, {
  initialVariables: {
    limit: 100
  },
  fragments: {
    viewer: () => Relay.QL`
      fragment on Viewer {
          dmcConnection(first: $limit) {
            edges{
              node{
                name
              }
            } 
          }        
      }
    `,
  },
});

When I click on a li I want to show another component with the detail of dmc object. How can I start?
Feel free to close this if you consider it's irrelevant.
Thank you as always.

[Feature] Dataloader

Possible solution:
Base64 encode the Request params and create a DataLoader for every function using the base64 encoded string as key.

filter arg not supported by Relays variables

example query

  initialVariables:{
    userSearch: 'user@email'
  },
  fragments: {
    query: () => Relay.QL`
      fragment on Viewer{
        userSearch(filter: {EmailSearch: $userSearch}, limit: 5) {
          firstName
          lastName
        }
      }
    `,
  }

This will not work as Relay variables are only supported as top-level arguments.

This would work:

  initialVariables:{
    userSearch: 'user@email'
  },
  fragments: {
    query: () => Relay.QL`
      fragment on Viewer{
        userSearch(email: $userSearch, limit: 5) {
          firstName
          lastName
        }
      }
    `,
  }

Should the filter params be moved to top level and mark sort and limit as specials.. ?

addRelation resolver projection does not cover UnionType resolveType()

Hi, I have some situation to use UnionType in my Type Relation and stuck with projection field doesn't coverage to my Resolver resolveType() method in GraphQLUnionType

const relatedResolver = HerbariumTC.getResolver('findMany')
    .clone()
    .setType(new GraphQLList(PlantSearchResultItemType));

  HerbariumTC.addRelation('Related', () => ({
    resolver: relatedResolver,
    args: {
      filter: (source) => {
        console.log(source); // >>> come out with projection fields
        return ({
          displayLocation: source.displayLocation,
        });
      },
    },
    projection: { displayLocation: 1 },
  }));
const PlantSearchResultItemType = new GraphQLUnionType({
    name: 'PlantSearchResultItem',
    types: [GenericPlantType, HerbariumTC.getType(), GardenTC.getType(), MuseumTC.getType()],
    resolveType(value) {
      console.log(value); // come with only _id
      if (value.museumLocation) {
        return MuseumTC.getType();
      } else if (value.zone) {
        return GardenTC.getType();
      } else if (value.cuid || value.displayLocation) {
        return HerbariumTC.getType();
      }
      return GenericPlantType;
    },
  });

Is this a correct way to do it ? 🤔🤔

or if this maybe a bug, I am now going to find in addRelation and projection

List/Array of Type

I am creating an addResolver and one of the args is a list of OtherSchema:

import OtherModelTC from './Schema'

...

ThisModelTC.addResolver({
  name: 'Resolver',
  type: 'mutation',
  args: {
    otherModel:  [OtherModelTC]
  }
})

How do I set that up? I'm getting errors if I wrap it within brackets

Thanks

[Bug] deeper projections

after this commit 1b26bf47 deeper projections dont work.

example:

HouseTC.addRelation(
  'people',
  () => ({
    resolver: UserTC.getResolver('connection'),
    args: {
      filter: (source) => ({ 
        _operators: {
          _id: {
            in: source.peopleIds.map(p => p.people) || []
          }
        } 
      }),
    },
    projection: { peopleIds: { people: true } },
  })
)

[Example] Geo query

@nodkz
Could you help me out and add a example for a connection+geo query example?

{
  viewer{
    feed(first: 10, longitude: 10.000, latitude: 20.000){
      edges {
        node {
          id
        }
      }
    }
  }
}

Guess this will be complex as you can not have a raw mongodb query near ?

rootValue and authed user

Could you explain how to get the current user if you have a id set at graphql rootValue

the id is accessed from rootValue.id

{
  viewer{
    user{
      firstName // of current user
      lastName // of current user
      email // of current user
    }
  }
}

Schema file have this right now:

const fields = {
  user: UsersTC.getResolver('findOne').getFieldConfig()
}
ViewerTC.addFields(fields)

What Phase

Is this project still in Pre Production? If not when do you plan on moving it towards production?

Using custom fields for creating new fields

Hello, everyone.
Few days ago I had a problem with case like this:
I have a schema

const UserSchema = new Schema({
  name: String,
  city: String,
},
{
  collection: 'users'
});
const User = mongoose.model('User', UserSchema);
const UserTC = composeWithRelay(composeWithMongoose(User));
...

And I added one custom field

UserTC.addFields({
  foo: {
    type: GraphQLString,
    projection: { name: true, city: true },
    resolve: (source) => (`${source.name} from ${source.city}`),
  },
});
...

And when I add another custom field depended from first custom field, I expected that field foo would be defined, and I can easy use It

UserTC.addFields({
  bar: {
    type: GraphQLString,
    projection: { foo: true },
    resolve: (source) => (`${source.foo} likes graphql-compose`), // but source.foo is undefined
  },
});

So, I think it would be a good functionality or What is the best way for resolving situation like this?
Thanks

Connection with pivot?

Hi,
This is a merge of MongoDB and GraphQL compose question. but i will try

How would you solve this?
Can it be solved with only 2 tables?. i want to make shure that there are different types of employees in the company

//User Table
{
  id: userId,
  firstName: 'Frank',
  lastName: 'Sinatra',
  email: '[email protected]'
}

//Company table
{
  id: companyId,
  name: 'Wallmart',
  description: 'awsome'
}

//Employeeconnection
{
  user: userId,
  company: companyId,
  employeeStatus: 'Cleaner'
}

[Mutations] - wrap functions

have i misunderstood it or is it possible to wrap your own resolvers for the mutators?

Lets say a user should create a message.
He can only create the message if he is admin.

MessageTC.getResolver('createOne').getFieldConfig()

Is it possible to expand this?

[Bug] deeper Relations

Hi,

UserModel:

// Create mongoose schema 
var UserSchema = new mongoose.Schema({
  email : {
    type: String, 
    unique: true, 
    required: true,
  },
  fileId: { 
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Files',
  },

}, {collection: 'Users' })

export const Users = mongoose.model('Users', UserSchema)
export const UsersTC = composeRelay(composeMongoose(Users))

UsersTC.addRelation(
  'myFile',
  () => ({
    resolver: ResourcesTC.getResolver('findById'),
    args: {
      _id: (source) => source.fileId,
      limit: null,
      sort: null,
    }
  })
)

GQL Query looks like:

{
  viewer{
    user{
      email
      myFile{
        url
      }
    }
  }
}

Working Schema:

composeFromRelay(GQC.rootQuery())

const ViewerTC = GQC.get('Viewer')
GQC.rootQuery().addField('viewer', {
  type: ViewerTC.getType(),
  description: 'Data under client context',
  resolve: () => ({}),
})

ViewerTC.addFields({
  user: UsersTC.getResolver('findMany').getFieldConfig(),
})

export default GQC.buildSchema()

This is NOT working:

composeFromRelay(GQC.rootQuery())

const ViewerTC = GQC.get('Viewer')
GQC.rootQuery().addField('viewer', {
  type: ViewerTC.getType(),
  description: 'Data under client context',
  resolve: () => ({}),
})

ViewerTC.addRelation(
  'user',
  () => ({
    resolver: UsersTC.getResolver('findById'),
    args: {
       _id: (source, args, context) => context.userId,
    },
  })
)
export default GQC.buildSchema()

.wrapResolve for fields

Is there anyway to create rules for changing fields

For example:

order.js

const orderSchema = new Schema({
  orderId: String,
  status: {
    type: Number,
    enum: [0,1,2,3,4]  // 0 = pending, 1 = processing, 2 = en route, 3 = delivered, 4 = cancelled ...
  },
  deliveryAddress: String,
}

I want to make a rule that if status > 1, the deliveryAddress cannot be changed in the mutation orderUpdate: OrderTC.getResolver('updateById'). Another rule is status can never move down, if has been moved to 1 it cannot go back to 0 etc.

Thanks!

Use regex expression filters

In the following example:

ViewerTC.addField('dmcList', DmcTC.getResolver('findMany').getFieldConfig());

I want to add a field 'dmcSearch' to filter using a regex expression.
Where can I start? I have been trying with something like:

ViewerTC.addField('dmcSearch', {
  type: DmcTC.getType(),
  args: {
    expression: { type: GraphQLString }
  },
  resolve: (dmc, {expression}) => {

  }
});

Thanks in advance.

Write retry middleware

This experience also caused us to rethink how we wanted to handle retries and backoff. We were hoping to keep all retry logic on the client for operational simplicity, but, if just one internal request had a transient failure, a client-side retry would not only waste all the work that did succeed from the first request, it would be at risk for its own transient failure. Since most of our backend failures and timeouts were transient, rather than consistently slow, we added a retrying wrapper around request-promise in our GraphQL server to re-do those requests. This simple change eliminated 99% of the errors when resolving queries, and the expense of a second or two of extra latency in cases where a retry occurred after a timeout.
From: https://fabric.io/blog/building-fabric-mission-control-with-graphql-and-relay

[Proposal] Refactor Resolver

1. Remove ResolverMiddleware, should be wrap

Simplify Resolver via removing ResolverMiddleware. Using of ResolverMiddleware is too complicated. I always forget how it works, and view its source code. Remove methods composeArgs, composeResolve and composeOutputType.

class Resolver {
  wrap((newResolver, prevResolver) => newResolver): newResolver;
  wrapResolve((next) => (resolveParams) => resolveFn): newResolver;
  wrapArgs((prevArgs) => newArgs): newResolver;
  wrapOutputType((prevOutputType) => newOutputType): newResolver;
}

Method wrap is the common method that can clone parent Resolver and override/wrap it.
Methods wrapResolve, wrapArgs, wrapOutputType are the helpers that work on top of the wrap method.

2. Remove cross-references with TypeComposer

Cross-reference between TypeComposer and Resolver should be removed. It brings unnecessary complexity and some bugs, which occurs when cloning resolver with existed name.

Resolver will not have TypeComposer in its props anymore. TypeCompose can be provided via wrap callback

TypeComposer may have a list of resolvers with arbitrary names. This name may not coincide with resolver name. Previously cross-reference updates resolver if it was wrapped. But it works buggy and unclear.

If you want to change Resolver in TypeComposer you should do it explicitly. Eg:

const PostTC = composeWithMongoose(PostModel);
const findManyResolver = PostTC.getResolver('findMany');
const newWrappedResolver = findManyResolver.wrap((newResolver, prevResolver) => {
  // some newResolver changes (eg. resolve method, args or outputType)
  // btw PostTC is available from parent scope ;)
});
PostTC.setResolver('findMany', newWrappedResolver);

3. Improve debugging

Debugging is some piece of hell when middleware has an error. Resolver wrapping should keep reference to parent Resolver. It should help to print resolvers chain and it's source code:

class Resolver {
  parent: Resolver;
  toString(): string;
}

// getNestedName() should return
wrapArgs(wrapRelay(findMany))

// toString() should return
Resolver(
  displayName: string,
  outputType: stringified type,
  args: stringified args,
  resolve: js code,
  parent: Resolver(
    ...
    parent: Resolver(
      ...
    )
  )
)

4. Rename RootQuery and RootMutation

Due to the convention, it should be called Query and Mutation.


This proposal is not final and may be changed.

[Feature] Add ability to pass fieldConfig to `addRelation` method.

TypeComposer.addRelation is a good solution for solving hoisting problem. It's defined via arrow function and converted to the field with config in GQC.buildSchema method.

Eg. in AuthorTC you relate PostTC and in PostTC you add AuthorTC (you may get as undefined first or second TC). So methods like setFields can not solve this problem, but addRelation does.

Right now addRelation works only with Resolver, for example:

// author.js
AuthorTC.addRelation(
  'posts',
  () => ({
    resolver: PostTC.get('$findMany'),
    args: {
      filter: source => ({ authorId: source._id }),
    },
    projection: { _id: 1 },
  })
);

// post.js
PostTC.addRelation(
  'author',
  () => ({
    resolver: AuthorTC.get('$findById'),
    args: {
      _id: source => source.authorId,
    },
    projection: { authorId: 1 },
  })
);

But in some cases, will be useful to pass fieldConfig directly to the relation. For one time using it's too dirtily to define Resolver, too much typing.
So let's allow to pass fieldConfig to the addRelation method. Eg.

// author.js
AuthorTC.addRelation(
  'hottestPost',
  () => ({
    type: PostTC,
    resolve: (source, args, context, info) => {
       const authorId = source._id;
       return PostModel.findFromSomeMongooseStaticMethod(authorId);
    },
    projection: { _id: 1 },
  })
);

To avoid errors Resolve thunk will check a presence of resolver property and absence of type and resolve properties and vice versa at the build schema step.

Add ability to set a defaultValue in the InputTypeComposer

A method that would allow setting a defaultValue for a specific field. E.g.,

var MyType = new GraphQLObjectType({
  name: 'MyType', 
  fields: { 
    flag: { type: GraphQLBoolean },
    value: { type: GraphQLString } 
  }
);
var tc = new TypeComposer(MyType);
var itc = tc.getInputTypeComposer();
itc.setDefaults({ flag: true, value: 'mydefaultvalue' });

FlowType errors

Hi!
I've got some Flow errors using graphql-compose. Seems it's right, there is no lib/resolver directory:


> [email protected] flow /Users/rax/sandbox/graphql-compose-swagger
> flow; test $? -eq 0 -o $? -eq 2

node_modules/graphql-compose/lib/definition.js.flow:27
 27: import type Resolver from './resolver/resolver';
                               ^^^^^^^^^^^^^^^^^^^^^ ./resolver/resolver. Required module not found

node_modules/graphql-compose/lib/definition.js.flow:28
 28: import type ResolverList from './resolver/resolverList';
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^ ./resolver/resolverList. Required module not found

node_modules/graphql-compose/lib/inputTypeComposer.js.flow:30
 30:       // $FlowFixMe
           ^^^^^^^^^^^^^ Error suppressing comment. Unused suppression

node_modules/graphql-compose/lib/resolver.js.flow:179
179:       // $FlowFixMe
           ^^^^^^^^^^^^^ Error suppressing comment. Unused suppression

node_modules/graphql-compose/lib/resolver.js.flow:181
181:         // $FlowFixMe
             ^^^^^^^^^^^^^ Error suppressing comment. Unused suppression

node_modules/graphql-compose/lib/resolver.js.flow:184
184:         // $FlowFixMe
             ^^^^^^^^^^^^^ Error suppressing comment. Unused suppression

node_modules/graphql-compose/lib/resolver.js.flow:191
191:     // $FlowFixMe
         ^^^^^^^^^^^^^ Error suppressing comment. Unused suppression

node_modules/graphql-compose/lib/resolver.js.flow:195
195:     // $FlowFixMe
         ^^^^^^^^^^^^^ Error suppressing comment. Unused suppression

node_modules/graphql-compose/lib/resolver.js.flow:340
340:       // $FlowFixMe
           ^^^^^^^^^^^^^ Error suppressing comment. Unused suppression

node_modules/graphql-compose/lib/toInputObjectType.js.flow:121
121:   // $FlowFixMe
       ^^^^^^^^^^^^^ Error suppressing comment. Unused suppression

node_modules/graphql-compose/lib/typeComposer.js.flow:45
 45:       // $FlowFixMe
           ^^^^^^^^^^^^^ Error suppressing comment. Unused suppression

node_modules/graphql-compose/lib/typeComposer.js.flow:145
145:         // $FlowFixMe
             ^^^^^^^^^^^^^ Error suppressing comment. Unused suppression

node_modules/graphql-compose/lib/typeMapper.js.flow:189
189:     if (type || (args && args !== fieldConfig.args)) {
                                                   ^^^^ property `args`. Property not found in
189:     if (type || (args && args !== fieldConfig.args)) {
                                       ^^^^^^^^^^^ object literal

node_modules/graphql-compose/lib/typeMapper.js.flow:192
192:         // $FlowFixMe
             ^^^^^^^^^^^^^ Error suppressing comment. Unused suppression

node_modules/graphql-compose/lib/typeMapper.js.flow:194
194:         args: args || fieldConfig.args,
                                       ^^^^ property `args`. Property not found in
194:         args: args || fieldConfig.args,
                           ^^^^^^^^^^^ object literal

node_modules/graphql-compose/lib/typeMapper.js.flow:228
228:         // $FlowFixMe
             ^^^^^^^^^^^^^ Error suppressing comment. Unused suppression

node_modules/graphql-compose/lib/typeMapper.js.flow:242
242:         // $FlowFixMe
             ^^^^^^^^^^^^^ Error suppressing comment. Unused suppression

node_modules/graphql-compose/lib/typeMapper.js.flow:303
303:         // $FlowFixMe
             ^^^^^^^^^^^^^ Error suppressing comment. Unused suppression


Found 18 errors

TypeComposer clones its resolvers type

Hi, in issue graphql-compose/graphql-compose-mongoose#23 I touched roughly following topic about clone fn on TypeComposer

let OccupationTC = composeWithMongoose(OccupationModel);
let OccupationStaffTC = OccupationTC.clone("OccupationStaff");

And later I use

SomeOtherTC.addRelation(

 "occupations",
 () => ({
    resolver: OccupationStaffTC.get('$findMany')
  })
);

The type, which this resolver returns, is the original Occupation and not the cloned OccupationStaff.

Is this correct?

Here TypeComposer clones all of its resolvers providing cloned TypeComposer as param
https://github.com/nodkz/graphql-compose/blob/master/src/typeComposer.js#L419

But is it, what Resolver expects here?
https://github.com/nodkz/graphql-compose/blob/master/src/resolver.js#L365

Isn't it the reason, why the type of the cloned TC's resolvers remain pointing to original TC?

Thanks
Jan

[Feature] Add method `Resolver. addFilterArg` which allows creating custom filters

Proposal of setting custom filter args, that affect on query:

// create complex input type for filtering by price range
InputTypeComposer.create(`input IntRange {
  # min value (this comment will be passed to the field description in schema)
  gt: Int
  # max value
  lt: Int
}`);

ViewerTC.addField(
  'shopList',
  ShopTC
    .getResolver('findMany')
    .addFilterArg({
      name: 'nameRegexp',
      type: 'String', // also can be 'Int Float Boolean ID String! [String] AnyNamedType' 
      description: 'Search by regExp',
      query: (rawQuery, value, resolveParams) => {
        rawQuery.name = new RegExp(value, 'i');
      },
    })
    .addFilterArg({
      name: 'tags',
      type: '[String]',
      description: 'Search by $in operator',
      query: (rawQuery, value, resolveParams) => {
        if (value && value.length > 0) {
          rawQuery.tags = { $in: value };
        }
      },
    })
    .addFilterArg({
      name: 'q',
      type: 'String',
      description: 'Fulltext search with mongodb stemming and weights',
      query: (rawQuery, value, resolveParams) => {
        // Do not forget create Text Index in your schema eg.
        // ShopSchema.index({
        //   title: 'text',
        //   description: 'text',
        //   tags: 'text',
        // }, {
        //   name: 'ShopTextIndex',
        //   default_language: 'russian',
        //   weights: {
        //     tags: 7,
        //     title: 5,
        //     // rest fields get weight equals to 1
        //   },
        // });
        resolveParams.args.sort = {
          score: { $meta: 'textScore' },
        };
        rawQuery.$text = { $search: value, $language: 'ru' };
        resolveParams.projection.score = { $meta: 'textScore' };
      },
    })
    .addFilterArg({
      name: 'priceLH',
      type: 'IntRange', // example of NamedType
      description: 'Search by price range',
      defaultValue: { gt: 0, lt: 100},
      query: (rawQuery, value, resolveParams) => {
        if (value.gt || value.lt) {
          const price = {};
          if (value.gt) price.$gt = value.gt;
          if (value.lt) price.$lt = value.lt;
          rawQuery.price = price;
        }
      },
    })
    .addFilterArg({
      name: 'geo',
      type: `input GeoDistance {
        lng: Float!
        lat: Float!
        # Distance in meters
        distance: Int!
      }`
      description: 'Search by dinstance in meters (`2dsphere` index on `geo` field)',
      query: (rawQuery, value, resolveParams) => {
        if (!value.lng || !value.lat || !value.distance) return;
        rawQuery.geo = {
          $nearSphere: {
             $geometry: {
                type: "Point",
                coordinates: [ value.lng, value.lat ]
             },
             $maxDistance: value.distance
          };
        }
      },
    })
    .addFilterArg({
      name: 'ageRange',
      type: InputTypeComposer.create('input AgeRange { min: Int, max: Int }'),
      description: 'Filter by age range (in years)',
      query: (rawQuery, value) => {
        const d = new Date();
        const month = d.getMonth();
        const day = d.getDate();
        const year = d.getFullYear();
        let minAge = value.min || 0;
        let maxAge = value.max || 0;
        if (!minAge && !maxAge) return;
        if (minAge > maxAge && minAge && maxAge) [minAge, maxAge] = [maxAge, minAge];
        rawQuery.birthday = {};
        if (maxAge) {
          rawQuery.birthday.$gte = new Date(year - maxAge - 1, month, day);
        }
        if (minAge) {
          rawQuery.birthday.$lte = new Date(year - minAge, month, day);
        }
      },
    })
    // DEBUG mongoose query
    // .wrapResolve((next) => (rp) => {
    //   const r = next(rp);
    //   console.log('RawQuery:', rp.rawQuery);
    //   console.log('Mongoose query:', rp.query);
    //   return r;
    // })
    .getFieldConfig()
);

Also from [email protected] available method Resolver.wrapCloneArg which allows to clone argument type and extend it:

// from `findMany` creating new 'findManyForAdmins' resolver and extending only its `filter` argument
ShopTC.setResolver('findManyForAdmin',
  ShopTC.get('$findMany')
  .wrapCloneArg('filter', 'FilterFindManyShopAdminInput')
  .addFilterArg({ ... })
);

// from `findMany` creating new 'findManyForUser' resolver and removing some fields from only its `filter` argument
ShopTC.setResolver('findManyForUser',
  ShopTC.get('$findMany')
  .wrapCloneArg('filter', 'FilterFindManyShopUserInput')
);
ShopTC.get('$findManyForUser.@filter').removeField(['price', 'geo', 'tags']);

// so `findMany` resolver will have `filter` arg with type FilterFindManyShopInput
// `findManyForAdmin` resolver will have another extended `filter` arg with type FilterFindManyShopAdminInput
// `findManyForUser` resolver will have cutted `filter` arg with type FilterFindManyShopUserInput

Creating type for API Service

Hi I want to make a GraphQL Query type for an API Service (like Uber API or Reddit API etc). How would I resolve the Type to an api response.

[bug] Complex queries throu dotted notations

I dont know if it is a bug or me writing it wrong.. 😟

working

UsersTC.addRelation(
  'family',
  () => ({
    resolver: UsersTC.getResolver('findMany'),
    args: {
      filter: source => ({
        _operators: {
          _id: {
            in: source.familyIds || []
          }
        } 
      })
    },
    projection: { familyIds: true },
  })
)

not working

UsersTC.addRelation(
  'family',
  () => ({
    resolver: UsersTC.getResolver('findMany'),
    args: {
      filter: source => ({
        _id: { $in: source.familyIds || [] }
      })
    },
    projection: { familyIds: true },
  })
)

Make addFilterArg asynchronous

I am using relay and graphql-compose, and I want to make a query for Item which are child of categories.
My schema looks like:

var CategorySchema = mongoose.Schema({
  site: Schema.Types.ObjectId,
  node: String,
});
var ItemSchema = mongoose.Schema({
  site: Schema.Types.ObjectId,
  ASIN: {type: String, unique: true},
  category: {type: Schema.Types.ObjectId, index: true},
});

And my filter looks like:

const findManyResolver = ItemTC.getResolver('connection')
  .addFilterArg({
    name: 'Category',
    type: 'String',
    query: (query, value, resolveParams) => {
      var site = await Site.get();
      query.category = await Category.findOne({site, node: value}).exec();
    },
  });
ItemTC.setResolver('connection', findManyResolver);

My problem is that I cannot make asynchronous database queries in addFilterArg.

From the client, I want to query and sort Items which are under a certain category where I know the node string, but not the _id.

.wrapResolve twice

Hi,

Can I use .wrapResolve twice

example:

createProduct:  ProductTC.getResolver('createOne')
  .wrapResolve(next => res => isBussinessOwner(res) ? next(res) : null)
  .wrapResolve(next => res => isSystemAdmin(res) ? next(res) : null)

Note: The above can be refactored into 1 wrapResolve, but I put it as a sample as I want to make a function the iterates over all mutations and wrapResolve with isAdmin(res)

[Feature] Add method `Resolver.addSortArg` which allows add custom sorting

According to Resolver.addFilterArg will be nice if we can add sorting with custom logic in the same way.

ViewerTC.addField(
  'shopList',
  ShopTC
    .getResolver('findMany')
    .addSortArg({
       name: 'UPDATED_AT_ASC',
       description: ' simple sorting',
       value: { updatedAt: 1 },
    })
    .addSortArg({
      name: 'RELEVANCE',
      description: 'Sort by score weight if text provided. Otherwise sort by date.',
      value: (resolveParams) => {
        if (resolveParams.args.filter.q) {
          // mongodb sorting by weight. Docs https://docs.mongodb.com/manual/core/text-search-operators/
          return { score: { $meta: 'textScore' } };
        }
        return { createdAt: -1 };
      },
    })
    .addSortArg({
      name: 'NONNULL_PRICE_ASC',
      description: 'Sort by price from low to hight, ommiting zero priced records',
      // deprecationReason: 'This sorting can be deprecated.',
      value: (resolveParams) => {
        resolveParams.query.where({ price: { $gt: 0 }}); 
        return { price: 1 };
      },
    })

Pagination using cursors

Hello there,
Thanks for this work I really find it helpful. Now I have a question.. is there anyway that I use cursors to paginate? Please note that my situation is to show a table with "count" rows (usually 10 rows per page) and with a control to go through the whole pages of the table, the data does not change much and the user can jump to any page he wants, I was thinking of a way to predict the cursor at a giving offset. is that possible?

Adding auth to fields

Hi, thanks again for awesome library. I am new to GraphQL so still learning. I have the following schema:

const UserSchema = new Schema({
  name: String,
  secret: String
});

Lets say I create a UserTC from this schema and I want the secret field to only be displayed if the logged in user (stored in req.user as in req.user.id == user._id), how do I do that? Is there a way to wrap the fields of UserTC so that whenever secret is accessed, I can check the req object so see if it should return it or return null instead?

directly create a document from a type

Let's say I have UserTC. I know i can do UserTC.find({ _id: id }).exec(...). Can I create a document/record the same way. Like UserTC.create(req.body).save() ?

Authentication / Authorization

How do I add authentication/authorization/permissions to queries and mutations?

example, User can view all users but can only edit his user account.
User who is admin of a business can add/remove users from the business and can make/receive payments
User who is an employee of a business can view payments made to/from the business

[Feature] Raw filter

The posibility to add a custom raw filter to the query.
resolveParams.args.rawFilter

rawQueryHelper(resolveParams)

resolve: (resolveParams : ExtendedResolveParams) => {
  resolveParams.query = model.find();
  rawQueryHelper(resolveParams);
  filterHelper(resolveParams);
  skipHelper(resolveParams);
  limitHelper(resolveParams);
  sortHelper(resolveParams);
  projectionHelper(resolveParams);
  return resolveParams.query.exec();
},

if so would possible need fixes in the relay and connections repo?

[Feature] Allow to pass type as function in TypeComposer and InputTypeComposer for fields

Feature:

Allow to pass type as function

TC.setFields({
  field1: {
    type: () => SomeGraphQLObjectType,
    description: 'Type as function. It will be resolved at schema build',
  }
});

Allow to pass FieldConfig as function

TC.setFields({
  field1: () => {
    type: SomeGraphQLObjectType,
    description: 'FieldConfig as function. It will be resolved at schema build',
  }
});

// Or shortly, providing only type
TC.setFields({
  field1: () => SomeGraphQLObjectType,
});

Problem: When implementing graphql-compose-elasticsearch module I got two input types that required each other:

Bool.js

import { InputTypeComposer } from 'graphql-compose';
import QueryITC from './Query';

const BoolITC = InputTypeComposer.create('ElasticQueryBool');
BoolITC.setFields({
  must: {
    type: QueryITC,
  },
  filter: {
    type: QueryITC,
  },
  ...
});

export default BoolITC;

Query.js

import { InputTypeComposer } from 'graphql-compose';
import BoolITC from './Bool';

const QueryITC = InputTypeComposer.create('ElasticQuery');
QueryITC.setFields({
  bool: {
    type: BoolITC,
  },
  ...
});

export default QueryITC;

In such case, we have hoisting problem (one of the types will be undefined). And we can not use addRelation method which solves this problem (it's only available in TypeComposer, not InputTypeComposer).

So type as a function will solve this problem in the following manner:

QueryITC.setFields({
  bool: () => BoolITC.getType(),
  ...
});

[$updateById] - updating to a dupe key

Returns error when updating a field to a value that already exists when key is set to unique.
Mongo will report back error msg but it will not be handled correct.
(Only tested using Relay)

complex queries

Is there a way to make complex queries + sort options?

{
   $and: [
      { "a.b.c": 1 }
      { "$or":  [
         "a.b.d": 2,
         "a.b.e": 2 
      ] }
   ]
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.