Giter VIP home page Giter VIP logo

prisma-binding's People

Contributors

a-type avatar alvis avatar brunoscheufler avatar fluorescenthallucinogen avatar frandiox avatar hirejohnsalcedo avatar kbrandwijk avatar kuldar avatar marktani avatar maticzav avatar nikolasburk avatar pantharshit00 avatar renovate-bot avatar renovate[bot] avatar schickling avatar timsuchanek avatar vakrim avatar zrthxn 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

prisma-binding's Issues

Binding API

I have played around with the current binding API, and it has a few pros and cons. There's a couple of possible directions, that I'd like to outline.

GraphQL server 'as a database'

The Graphcool GraphQL server closely mimics a database. This might validate an API that's closer to traditional ORMbinding libraries. These libraries are more centered around a Type, instead of a query/mutation, and would look something like this (inspired by libraries like Mongoose, Linq, etc.):

schema.Post.find({ name: 'abc' }) //or: schema.Post.findOne(...) and schema.Post.findMany(...)
new schema.Post({ name: 'abc' })

This type of API can only be achieved when the mapping between types and their corresponding query and mutation root fields is known. For Graphcool, they are, for generic GraphQL servers, they are not. This could be implemented using directives in the schema, for example:

type Query {
   myPosts: [Post] @findMany
}

Generic GraphQL server

As seen above, the 'database like approach' required either convention or configuration to work, and would only work well for GraphQL schemas that actually mimic a traditional database design. The alternative is to stick to the root query and mutation fields, like the current implementation:

schema.allPosts(...) // or: schema.topPosts(...) or schema.posts(...)

This stays closest to the original schema you are binding to. I think this is a better fit for GraphQL binding.

Filtering and other parameters

Queries can have parameters. Most commonly used for filtering, ordering, aggregating etc. Currently, they are passed as arguments to the binding method. It might be worth investigating if field parameters can also become part of the API itself. So:

schema.posts({filter: { name: { starts_with: 'Hello' } } })

Could also be written as:

schema.posts.filter({ name: { starts_with: 'Hello' } })

Or even (when also generating types for GraphQL types):

schema.posts.filter(Post.name.startsWith('Hello'))

These calls could be chained too:

schema.posts.filter(...).orderBy(...)

This needs a lot more investigating to see if it can easily be determined which functions to generate. Maybe based on a check for scalar fields, to prevent generating fields for something like:

schema.post({ id: '123'})
schema.post.id('123')

Chaining

There was already an example of chaining in the previous chapter, where filter() and orderBy() were combined. But chaining also offers a possibility to implement functionality that is not offered by the server. For example, if the server doesn't offer aggregation options, this might be implemented as a function in the binding library:

schema.posts().aggregate('{ age }').avg('{ weight }').groupBy('{ age }')

This would be equivalent to SELECT age, avg(weight) FROM posts GROUP BY age.
I believe the possibility to create your own binding, and add functionality like this in an easy way at the binding level, instead of the consuming resolver, would make it a lot easier to implement certain features.

Prisma binding class should not use global variables

typeDefsCache and remoteSchema are currently variables outside the Prisma class (making them global). This is done as a form of 'caching', so generating the remote schema does not happen on every request.

However, at the same time, this makes it impossible to use more than one Prisma binding, because the global variables will be shared across those.

Bindings should be completely isolated from one another.

Create 'passthrough' resolvers for 1-on-1 delegation to underlying schema

Passthrough resolvers

In the current implementation, exposing root fields from one of the bindings in the final schema is a bit involved, because you need to:

  • Add the field to your schema (1)
  • Implement a resolver that delegates to the correct field on the binding (2)

This introduces a lot of duplication and boilerplate. This proposal addresses this.

Implementation details

To make step (1) easier, there will also be a proposal on graphql-import to support importing root fields using the existing # import syntax (-- insert link here --)
For step (2), the minimal setup requires at least to define the resolver, and specify which binding to pass through to. So the resolver could look like this:

const resolver = {
  Query: {
    posts: passthrough('database')
  }
}

With 'database' being the name of the binding, and passthrough being an exported function from graphcool-binding (or maybe re-exported from graphql-binding) that translates into the right resolver.
Based on the available information at runtime in the parent and info parameters, combined with the name of the binding, all information should be available to do this.

Doing a query in resolvers without specifing a fragment omits object properties?

Passing this fragment:

{
  id
  name
  description
  direction
  locations
  price
  spaces
  operation
  categories
  expenses {
    name
    price
  }
  images
  amenities {
    description
    icon
    type
  }
  featured
  summary {
    name
    value
  }
}

To this resolver:

registriesByQuery: async (parent, {query}, ctx) => {
    const registries = await ctx.db.query.registries({where: {operation: query.operation}}, FullRegistry) // <- Fragment here
    console.log(registries) // Returns the object below
  },

Returns the full object, that's fine:

  {
      "locations": [
        "zona sur",
        "quilmes",
        "bernal",
        "bernal este"
      ],
      "name": "Caseros 754",
      "description": "Casa 5 ambientes en Barrio Parque",
      "price": 250000,
      "featured": false,
      "direction": "Caseros 754, Barrio Parque, Bernal",
      "id": "cjcknlka700e201288tk49410",
      "operation": "venta",
      "amenities": [
        {
          "description": "3 dormitorios",
          "icon": "fa fa-bed",
          "type": "spaces"
        },
        {
          "description": "1 baño",
          "icon": "fa fa-bath",
          "type": "spaces"
        },
        {
          "description": "1 toilette",
          "icon": "fa fa-bath",
          "type": "spaces"
        },
        {
          "description": "Equipada",
          "icon": "fa fa-cutlery",
          "type": "spaces"
        },
        {
          "description": "Jardín",
          "icon": "fa fa-leaf",
          "type": "spaces"
        }
      ],
      "categories": [
        "casa"
      ],
      "images": [
        "http://placehold.it/256x256"
      ],
      "spaces": 3,
      "summary": [
        {
          "name": "Superficie cubierta",
          "value": "123 M2"
        }
      ],
      "expenses": []
    }

But if i don't pass the fragment, I got this back (missing all the fields that are arrays or nested):

{ id: 'cjcknlka700e201288tk49410',
    createdAt: '2018-01-18T15:31:06.000Z',
    updatedAt: '2018-01-18T15:31:06.000Z',
    name: 'Caseros 754',
    description: 'Casa 5 ambientes en Barrio Parque',
    direction: 'Caseros 754, Barrio Parque, Bernal',
    price: 250000,
    spaces: 3,
    operation: 'venta',
    featured: false }

I'm sure that i'm missing something but it would be nice to know what, thanks!

`exists` doesn't work?

I tried using the exists feature, but I can't get it to work. I checked the sources, and I don't see how it would work currently. The function name (e.g. for graphcool.exists.user(...) that would be user) is passed in as rootFieldName, but that doesn't exist, and the generated info object contains a different rootFieldName, which I think is also incorrect, because it should only contain the field selection. Also, it doesn't use proper pluralization, so it will fail there as well for all types ending in -o (Potato -> allPotatoes, not allPotatos).

Remove bundling from graphcool-binding?

Currently, graphcool-binding supports schema bundling out of the box. This is nice, but it creates a lot of dependencies. And it feels a bit like Express running Webpack for you. It think there is a clear pattern emerging that you use graphql-cli and its config file to configure and run bundling on your schemas to process imports, and this task should no longer be included in graphcool-binding.

API to combine multiple query fields into one request

In this case I would prefer to combine the two queries into one and use aliases to get the two results. Or does dataloader automatically combine them?

async realzTopPost(parent, args, ctx: Context, info) {
    const {containing} = args

    const [inTitle, inText] = await Promise.all([
      ctx.db.query.posts({first: 1, filter: {title_contains: containing}}, info).then(x => x.find(x => true)),
      ctx.db.query.posts({first: 1, filter: {text_contains: containing}}, info).then(x => x.find(x => true))])

    return inTitle || inText
  }

thanks to @sorenbs for reporting! 😎

Invalid selection set should throw error

This query has a invalid selection set. (logins should be login)

ctx.db.query.user({ where: { id: userId } }, '{ logins { id } }')

Currently this generates an invalid query:

1: query ($_where: UserWhereUniqueInput!) {
2:   user(where: $_where) {}
                           ^
3: }

It would be better to throw an exception

This issue is related to but different from #16

Doesn't generate subscription type correctly

Error message:

Argument of type 'string | GraphQLResolveInfo' is not assignable to parameter of type '{ [key: string]: any; }'

Reproduction:

npm install -g graphql-cli
graphql create my-app -b https://github.com/graphql-boilerplates/typescript-graphql-server/tree/f2f0bf5c76c8fce7fd47a5654ae71e7851d25f6b/basic
cd my-app
yarn start
 ~/p/p/b/t-b  yarn start
yarn start v0.27.5
warning package.json: No license field
$ ts-node src/index.ts

/Users/marktani/projects/playground/boilerplates/t-b/node_modules/graphcool-cli-core/node_modules/ts-node/src/index.ts:307
        throw new TSError(formatDiagnostics(diagnosticList, cwd, ts, lineOffset))
              ^
TSError: ⨯ Unable to compile TypeScript
src/generated/graphcool.ts (398,124): Argument of type 'string | GraphQLResolveInfo' is not assignable to parameter of type '{ [key: string]: any; }'.
  Type 'string' is not assignable to type '{ [key: string]: any; }'. (2345)
    at getOutput (/Users/marktani/projects/playground/boilerplates/t-b/node_modules/graphcool-cli-core/node_modules/ts-node/src/index.ts:307:15)
    at /Users/marktani/projects/playground/boilerplates/t-b/node_modules/graphcool-cli-core/node_modules/ts-node/src/index.ts:336:16
    at Object.compile (/Users/marktani/projects/playground/boilerplates/t-b/node_modules/graphcool-cli-core/node_modules/ts-node/src/index.ts:498:11)
    at Module.m._compile (/Users/marktani/projects/playground/boilerplates/t-b/node_modules/graphcool-cli-core/node_modules/ts-node/src/index.ts:392:43)
    at Module._extensions..js (module.js:584:10)
    at Object.require.extensions.(anonymous function) [as .ts] (/Users/marktani/projects/playground/boilerplates/t-b/node_modules/graphcool-cli-core/node_modules/t
s-node/src/index.ts:395:12)
    at Module.load (module.js:507:32)
    at tryModuleLoad (module.js:470:12)
    at Function.Module._load (module.js:462:3)
    at Module.require (module.js:517:17)
    at require (internal/module.js:11:18)
    at Object.<anonymous> (/Users/marktani/projects/playground/boilerplates/t-b/src/index.ts:2:1)
    at Module._compile (module.js:573:30)
    at Module.m._compile (/Users/marktani/projects/playground/boilerplates/t-b/node_modules/graphcool-cli-core/node_modules/ts-node/src/index.ts:392:23)
    at Module._extensions..js (module.js:584:10)
    at Object.require.extensions.(anonymous function) [as .ts] (/Users/marktani/projects/playground/boilerplates/t-b/node_modules/graphcool-cli-core/node_modules/t
s-node/src/index.ts:395:12)
error Command failed with exit code 1.

Removing this from src/generated/graphcool.ts (around line 398) makes the server work without any problems:

- subscription: Subscription = {
-   post: (args, infoOrQuery): Promise<AsyncIterator<PostSubscriptionPayload>> =>  super.delegateSubscription('post', args, {}, infoOrQuery),
-     user: (args, infoOrQuery): Promise<AsyncIterator<UserSubscriptionPayload>> => super.delegateSubscription('user', args, {}, infoOrQuery)
-  }

A similar, but different, error occurs for the boilerplate typescript-advanced:

graphql create my-app2 -b https://github.com/graphql-boilerplates/typescript-graphql-server/tree/f2f0bf5c76c8fce7fd47a5654ae71e7851d25f6b/advanced

support for multiple services?

One great feature with graphql is that one can combine multiple apis in one, with schema stitching or weaver schemas. The same applies here: one could have multiple underlying prisma services (with different contexts and/or different types of db) and it would be great to in single binder, something like this:

const prisma = new Prisma([
{
  typeDefs: 'schemas/sql_database.graphql',
  endpoint: 'https://us1.prisma.sh/demo/my-service1/dev'
  secret: 'my-super-secret-secret'
},
{
  typeDefs: 'schemas/neo4j_database.graphql',
  endpoint: 'https://us1.prisma.sh/demo/my-service2/dev'
  secret: 'my-super-secret-secret'
}
])

This comes from a real-world necessity and I thing some other people would also benefit from this

`exists` allows to use `post`

I have a Post type in my data model:

type Post {
  id: ID! @unique
  createdAt: DateTime!

  title: String!

  author: User! @relation(name: "UserPosts")
}

Now, when using the exist operator, it should only be possible to do graphcool.exists.posts(...). But in this case I can also do graphcool.exists.post(...) which doesn't give an error but rather executes the corresponding query (which then false due to wrong arguments).

Improve error message when calling mutation that doesn't exist

When calling a misspelled mutation like this:

ctx.db.mutation.updadteWorkspaceInvitation( [...] )

The following (unhelpful) error is returned:

TypeError: Cannot read property 'args' of undefined
    at processRootField (/Users/sorenbs/code/gc/cloud/node_modules/graphql-tools/src/stitching/delegateToSchema.ts:202:37)
    at /Users/sorenbs/code/gc/cloud/node_modules/graphql-tools/src/stitching/delegateToSchema.ts:127:15
    at Array.map (<anonymous>)
    at createDocument (/Users/sorenbs/code/gc/cloud/node_modules/graphql-tools/src/stitching/delegateToSchema.ts:125:28)
    at Object.<anonymous> (/Users/sorenbs/code/gc/cloud/node_modules/graphql-tools/src/stitching/delegateToSchema.ts:51:38)
    at step (/Users/sorenbs/code/gc/cloud/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:40:23)
    at Object.next (/Users/sorenbs/code/gc/cloud/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:21:53)
    at /Users/sorenbs/code/gc/cloud/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:15:71
    at Promise (<anonymous>)
    at __awaiter (/Users/sorenbs/code/gc/cloud/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:11:12)
    at Object.delegateToSchema (/Users/sorenbs/code/gc/cloud/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:49:12)
    at Proxy.<anonymous> (/Users/sorenbs/code/gc/cloud/node_modules/graphcool-binding/src/index.ts:169:14)
    at /Users/sorenbs/code/gc/cloud/src/resolvers/Mutation/workspace.ts:96:27
    at step (/Users/sorenbs/code/gc/cloud/src/resolvers/Mutation/workspace.ts:32:23)
    at Object.next (/Users/sorenbs/code/gc/cloud/src/resolvers/Mutation/workspace.ts:13:53)
    at fulfilled (/Users/sorenbs/code/gc/cloud/src/resolvers/Mutation/workspace.ts:4:58)

Settle API for `exists`

It feels a bit unnatural to use exists as follows:

ctx.db.exists.comments({
      writtenBy: {
        id: userId
      }
    })

What should the generated functions on exists be named?

Better error message when delegating selection set for scalar fields

Mutation:

mutation {
  updatePassword(password: String!): String!
}}

resolver:

ctx.db.mutation.updateLogin(
  {
    where: { id: loginId },
    data: { name: newName },
  },
  info
)

Results in this error:

GraphQLError: Field 'updateLogin' of type 'Login' must have a sub selection. (line 2, column 3):
  updateLogin(data: $_data, where: $_where)
  ^
    at Object.locatedError (/Users/sorenbs/code/gc/cloud/node_modules/graphql/error/locatedError.js:23:10)
    at Object.checkResultAndHandleErrors (/Users/sorenbs/code/gc/cloud/node_modules/graphql-tools/src/stitching/errors.ts:83:11)
    at Object.<anonymous> (/Users/sorenbs/code/gc/cloud/node_modules/graphql-tools/src/stitching/delegateToSchema.ts:91:14)
    at step (/Users/sorenbs/code/gc/cloud/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:40:23)
    at Object.next (/Users/sorenbs/code/gc/cloud/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:21:53)
    at fulfilled (/Users/sorenbs/code/gc/cloud/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:12:58)
    at <anonymous>
    at process._tickDomainCallback (internal/process/next_tick.js:228:7)

as there is no selection set in info. This makes sense, but can be confusing.

Could we provide a better error message?

Calling a variable "data" breaks mutation

You can find a see the bug using this repository: https://github.com/michaelchiche/prisma-variable-name-data

then running these two mutations:

mutation Works($post: PostCreateWithoutAuthorInput!) {
  createPostOK(post: $post) {
    id
  }
}

mutation Fails($post: PostCreateWithoutAuthorInput!) {
  createPostKO(data: $post) {
    id
  }
}

with this data for post:

{
  "post": {
    "title": "OK",
    "text": "I work fine with post",
    "isPublished": false
  }
}

and both mutations call this function:

function createPost(
  parent,
  post: PostCreateWithoutAuthorInput,
  context: Context,
  info,
) {
  const p: PostCreateInput = {
    ...post,
    author: {
      connect: {
        slug: 'michael',
      },
    },
  };
  return context.db.mutation.createPost({ data: p }, info);
}

Allow using plain object for GraphQLResolveInfo or some way to automate it

Consider schema where User have boughtProducts and each Product have a list of categories.

Consider you query for the user and want to have informations about all products he/she bought and all categories of those products.

Writing string 'query' to have all those informations resolved looks ugly and is not type-safe. In case of schema change, also, there is no easy way to know if it introduced some errors.

Consider using objects instead where you define what fields you need by setting them to true or nested object:

const productsWithDetails = await ctx.db.query.users(query, {
  boughtProducts: {
    price: true,
    categories: {
      name: true,
      id: true
    },
    // or even categories: '*',
  },
  name: true,
  age: true,
})

It will lead to a lot of true values, but it could have auto-generated interfaces and be type-safe so in case of schema changes you'd see errors, and also it'd be helpful to have IDE suggestions about such interface instead of writing plain string.

I'm aware such approach would introduce a new format and it might be a terrible idea. I'm just thinking about any easy and type-safe way to define nested data requirements of the queried object.

Also, maybe in case when you need all the fields of some object and their 1,2 level nested children, there is some way to generate such GraphQLResolveInfo object instead of writing it by-hand as string?

ctx.db.query.tasks(queryWhere, someTypeSafeDefinitionOf3LevelsNestedFields);

"Schema must be an instance of GraphQLSchema." when creating subscription binding

Executing this subscription resolver

  Subscription: {
    publications: {
      subscribe: async(parent, args, ctx, info) => {
        return ctx.db.subscription.post({}, info)
      },
    },
  }

leads to this error:

Schema must be an instance of GraphQLSchema. Also ensure that there are not multiple versions of GraphQL installed in your node_modules directory.

See https://github.com/marktani/reproduction-subscriptions for a complete reproduction.

Re-use application fields to filter data from database

It's a common scenario that you're adding a new (computed) field to your application schema. It would be great if there was a way to filter data from the database based on this field.

This might also require a new feature in the Graphcool database.

Leverage graphql-config extension for configuration context

const graphcool = new Graphcool({
  schemaPath: "schemas/database.graphql",
  endpoint: process.env.GRAPHCOOL_ENDPOINT,
  secret: process.env.GRAPHCOOL_SECRET,
})

Most information provided in the previous snippet is available in the .graphqlconfig file. It would be interesting to see whether this information could be read automatically without the need to specify it.

My current understanding is that the schemaPath can be always used and therefore can be left out if a .graphqlconfig file is available. The more difficult part is around the endpoint and secret as this information changes from stage to stage. This needs to be further researched.

Unify response from delegate and request

The delegates and the request method have different response formats. The request has data.myQuery, the delegates return the response directly. This makes it harder to interoperate.

Proposal: return the first element from data for request.

What's the benefit of having bindings on the context

Experimenting with the bindings, I found that they work equally well when not defined on the context, but just in the server index file. The added benefit is that they are only instantiated once, instead of for every request. What's the benefit of having them on the context, or is this a remnant of the early architecture?

How can I modify the behaviour of a subscription resolver

Consider this subscription resolver:

  Subscription: {
    publications: {
      subscribe: async (parent, args, ctx, info) => {
        return ctx.db.subscription.post({ }, info)
      },
    },
  },

As it is written, it will send events for any mutation to the Post model. However, I might want to cover different scenarios:

  • block deleted events
  • rename the title of the post in the subscription event being sent out

How can I handle these and other scenarios? Basically I want to intercept the subscription event with any custom logic.

Calling prisma after altering field has no effect when the info object is passed.

@Fonz001 commented on Tue Jan 30 2018

Current behavior

Calling prisma after altering field has no effect when the info object is passed.

Reproduction

  1. I have a simple Post model, matching with the typescript boilerplate from prisma init.
  2. Copied the endpoint: createPost(data: PostCreateInput!): Post! from prisma.graphql
  3. Implemented the resolver:
  async createPost(parent, args: { data: PostCreateInput }, ctx: Context, info) {
    const userId = getUserId(ctx)
   
    // Alter the title, we want it to be uppercase
    if (args.data.title) args.data.title = args.data.title.toUpperCase()

    // Double check
    console.log(args)

    // Pass the call to prisma
    return ctx.db.mutation.createPost(args, info)
  }

Expected behavior?

I expect to see the title to uppercase in the console. However, I see the original title being passed:

{ data: { title: 'FOO2', text: 'bar2', author: { connect: [Object] } } }
Request to https://eu1.prisma.sh/patrick-van-rietschoten/prisma-temp/dev:
query:
mutation {
  createPost(data: {title: "foo2", text: "bar2", author: {connect: {id: "cjd1br6zytcsv0113pp0xbnlk"}}}) {
    id
  }
}
operationName: null
variables:
{}
Response from https://eu1.prisma.sh/patrick-van-rietschoten/prisma-temp/dev:
{
  "createPost": {
    "id": "cjd1c6nvcukn80113rjfcprx1"
  }
}

Extra info:

Excluding the info parameter from the prisma call results in the expected behavior:

ctx.db.mutation.createPost(args)

{ data: { title: 'FOO2', text: 'bar2', author: { connect: [Object] } } }
Request to https://eu1.prisma.sh/patrick-van-rietschoten/prisma-temp/dev:
query:
mutation ($_data: PostCreateInput!) {
  createPost(data: $_data) {
    id
    createdAt
    updatedAt
    isPublished
    title
    text
  }
}
operationName: null
variables:
{
  "_data": {
    "title": "FOO2",
    "text": "bar2",
    "author": {
      "connect": {
        "id": "cjd1br6zytcsv0113pp0xbnlk"
      }
    }
  }
}
Response from https://eu1.prisma.sh/patrick-van-rietschoten/prisma-temp/dev:
{
  "createPost": {
    "updatedAt": "2018-01-30T07:45:27.000Z",
    "isPublished": false,
    "text": "bar2",
    "id": "cjd1c8yk4urma0113d2xpbtu1",
    "createdAt": "2018-01-30T07:45:27.000Z",
    "title": "FOO2"
  }
}

Cannot read property 'getMutationType' of undefined

I'm trying to produce a simple auth example, you can find the full repo here.

The project is setup as follows:

index.js

const { GraphQLServer } = require('graphql-yoga')
const { importSchema } = require('graphql-import')
const { Graphcool } = require('graphcool-binding')
const { me, signup, login, AuthPayload } = require('./auth')

const typeDefs = importSchema('./src/schema.graphql')
console.log(typeDefs)

const resolvers = {
  Query: {
    me,
  },
  Mutation: {
    signup,
  //   login,
  },
}

const server = new GraphQLServer({
  typeDefs,
  resolvers,
  context: req => ({
    ...req,
    db: new Graphcool({
      schemaPath: './database/schema.generated.graphql',
      endpoint: process.env.GRAPHCOOL_ENDPOINT,
      secret: process.env.GRAPHCOOL_SECRET,
    }),
  }),
})

server.start(() => console.log('Server is running on http://localhost:4000'))

schema.graphql

type Query {
  me: User
}

type Mutation {
  signup(email: String!, password: String!): AuthPayload!
  # login(email: String!, password: String!): AuthPayload!
}

type AuthPayload {
  token: String!
  user: User!
}

type User {
  id: ID!
  email: String!
  name: String!
}

auth.js

const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const { Context, getUserId } = require('./utils')

const AuthPayload = {
  user: async ({ user: { id } }, args, ctx, info) => {
    return ctx.db.query.user({ where: { id } }, info)
  }
}

// query the currently logged in user
function me(parent, args, ctx, info) {
  const id = getUserId(ctx)
  return ctx.db.query.user({ where: { id } }, info)
}

// register a new user
async function signup(parent, args, ctx, info) {
  const password = await bcrypt.hash(args.password, 10)
  const user = await ctx.db.mutation.createUser({
    data: { ...args, password },
  })

  return {
    token: jwt.sign({ userId: user.id }, process.env.JWT_SECRET),
    user,
  }
}

// log in an existing user
async function login(parent, { email, password }, ctx, info) {
  const user = await ctx.db.query.user({ where: { email } })
  if (!user) {
    throw new Error(`No such user found for email: ${email}`)
  }

  const valid = await bcrypt.compare(password, user.password)
  if (!valid) {
    throw new Error('Invalid password')
  }

  return {
    token: jwt.sign({ userId: user.id }, process.env.JWT_SECRET),
    user,
  }
}

module.exports = { me, signup, login, AuthPayload }

utils.js

const jwt = require('jsonwebtoken')
const { Graphcool } = require('graphcool-binding')

function getUserId(ctx) {
  const Authorization = ctx.request.get('Authorization')
  if (Authorization) {
    const token = Authorization.replace('Bearer ', '')
    const { userId } = jwt.verify(token, process.env.JWT_SECRET)
    return userId
  }

  throw new AuthError()
}

class AuthError extends Error {
  constructor() {
    super('Not authorized')
  }
}

module.exports = {
  getUserId,
  AuthError
}

I am getting an error when sending the signup mutation:

mutation {
  signup(email:"[email protected]" password: "asd") {
    token
  }
}

This is the full error message:

TypeError: Cannot read property 'getMutationType' of undefined
    at Object.getTypeForRootFieldName (/Users/nburk/Projects/graphcool/docs/migration-example/1.0/myapp/node_modules/graphql-binding/dist/utils.js:25:45)
    at buildInfoForAllScalars (/Users/nburk/Projects/graphcool/docs/migration-example/1.0/myapp/node_modules/graphql-binding/dist/info.js:17:24)
    at Object.buildInfo (/Users/nburk/Projects/graphcool/docs/migration-example/1.0/myapp/node_modules/graphql-binding/dist/info.js:7:16)
    at Proxy.<anonymous> (/Users/nburk/Projects/graphcool/docs/migration-example/1.0/myapp/node_modules/graphql-binding/dist/handler.js:15:27)
    at signup (/Users/nburk/Projects/graphcool/docs/migration-example/1.0/myapp/src/auth.js:20:38)
    at <anonymous>

Provide better error message for refused connection

@marktani commented on Wed Dec 20 2017

Error: request to http://localhost:60000/api/example-airbnb/dev failed, reason: connect ECONNREFUSED
127.0.0.1:60000
    at Object.checkResultAndHandleErrors (/Users/marktani/projects/playground/beta-1.1/graphcool-serv
er-example/node_modules/graphql-tools/src/stitching/errors.ts:84:7)
    at Object.<anonymous> (/Users/marktani/projects/playground/beta-1.1/graphcool-server-example/node
_modules/graphql-tools/src/stitching/delegateToSchema.ts:91:14)
    at step (/Users/marktani/projects/playground/beta-1.1/graphcool-server-example/node_modules/graph
ql-tools/dist/stitching/delegateToSchema.js:40:23)
    at Object.next (/Users/marktani/projects/playground/beta-1.1/graphcool-server-example/node_module
s/graphql-tools/dist/stitching/delegateToSchema.js:21:53)
    at fulfilled (/Users/marktani/projects/playground/beta-1.1/graphcool-server-example/node_modules/
graphql-tools/dist/stitching/delegateToSchema.js:12:58)
    at <anonymous>
    at process._tickDomainCallback (internal/process/next_tick.js:228:7)

My localdatabase_graphcool-database_1 container is on port 60001 and not 60000. We could return an error like

Error: request to http://localhost:60000/api/example-airbnb/dev failed, this is probably because your docker container localdatabase_graphcool-database_1 is not running on port 60000. Run docker ps to find out the port and update .env accordingly.

Export typeDefs from generated prisma.ts

Hi,

I would like to be able to access typedefs in the generated prisma.ts file.

Specifically changing the generated file from:

const typeDefs = `
# THIS FILE HAS BEEN AUTO-GENERATED BY "PRISMA DEPLOY"
# DO NOT EDIT THIS FILE DIRECTLY
...
`

to:

export const typeDefs = `
# THIS FILE HAS BEEN AUTO-GENERATED BY "PRISMA DEPLOY"
# DO NOT EDIT THIS FILE DIRECTLY
...
`

I'm happy to make a PR if this is something that would be accepted, and someone can point me in the right direction.

Permissions API

Hi, so I discussed this a bit with @marktani already and he suggested that I share it here, so we can have further discussion 😄

So, to begin with, from what I have read and from my experience I would say that it is best to separate things that might seem related in the app but are actually not. In my opinion it would be best to separate backend into business logic part, database part and security part. This way we can make our code a lot more manageable, as separate "systems" cannot interrupt each other and most importantly a lot easier to understand, as the logic of our app isn't mixed with security or database and vice-versa and everything is just very comprehensible.

I love how permissions are tackled in Graphcool services now. It's simply brilliant. I love that they are completely separate from my client and database and, best of all, that they thoroughly depend only on database, which is also completely independent. Because of this, I can test each permission separately and gradually add new ones if it need be, to suffice the requirements.

So it actually comes down to this, in my opinion:

  - operation: User.read
    authenticated: false
    fields:
      - name
      - school
  - operation: User.read
    authenticated: true
    fields:
      - name
      - email
      - photo
      - school
  - operation: User.read
    authenticated: true
    query: permissions/readUser.graphql

This is, in my opinion, the coolest thing about permissions; to be able to make "levels" of access - so that, referring to the example, a User with no authentication can access only certain fields and the one with access a bit more and if you are reading your properties you can read all of it.

I've heard about exists method, but it just feels very unnatural to me. For one, because it is mixed with resolvers, which makes development a lot less linear, because you can't just test queries. (Random thought (this is actually more a graphql-yoga concern maybe), it would be really cool to be able to have "admin" permissions in graphql-playground, very similar to what Graphcool offers, out of the box. I think that if we separate permissions from resolvers, this could be achieved very easily) .

So actually I have already given a suggestion now 😅, why don't we implement permissions the way resolvers are implemented in graphcool-yoga? Something like this maybe:

const server = new GraphQLServer({
  typeDefs,
  resolvers,
  permissions, // <--
  context: req => ({
    ...req,
    db: new Graphcool({
      schemaPath: DATABASE_SCHEMA_PATH,
      endpoint: process.env.GRAPHCOOL_ENDPOINT,
      secret: process.env.GRAPHCOOL_APIKEY,
    }),
  }),
  options: { port: 5000 },
})

Also I really love permission syntax, I think it would be really neat to just find a way to port it to JavaScript somehow or something similar or maybe just a "translator" (something like schema import).

Anyway, I hope it makes some sense, would love to discuss further on! 😄

Delegating to field with different type

I have the following data model:

type Post @model {
  id: ID! @isUnique
  title: String!
  author: User! @relation(name: "UserPosts")
  comments: [Comment!]! @relation(name: "CommentsOnPost")
}

type User @model {
  id: ID! @isUnique
  email: String! @isUnique
  password: String!
  name: String!
  posts: [Post!]! @relation(name: "UserPosts")
  comments: [Comment!]! @relation(name: "UserComments")
}

type Comment @model {
  id: ID! @isUnique
  text: String!
  post: Post! @relation(name: "CommentsOnPost")
  writtenBy: User! @relation(name: "UserComments")
}

Now, in my app.graphql I want specify the following root field:

# import Comment from "./database.graphql"

type Query {
  commentsForPost(id: ID!): [Comment!]!
}

Now, the straightforward way to implement this would be to delegate to query.comments and passed the post's id as a filter. However, it feels more "natural" to fetch the specific Post including its comments, but this doesn't really work with the API. this is what the resolver would look like:

  async commentsForPost(parent, { id }, ctx: Context, info) {
    const selection = `{ comments { id text } }`
    const post = await ctx.db.query.post({id}, selection)
    return post.comments
  }

This but the problem is the selection set is not complete. Once a query also asks for the writtenBy field of the comments, this will break because selection doesn't include that info.

Transforming an incoming selection set before passing it into a resolver

Say I have the following datamodel.graphql:

type Link {
  id: ID!
  url: String!
}

and the following schema.graphql:

# import Link from "./generated/graphcool.graphql"

type Query {
  feed: Feed!
}

type Feed {
  links: [Link!]!
  count: Int!
}

This is how my resolver currently looks like:

async function feed(parent, args, ctx, info) {
  const allLinks = await ctx.db.query.links({})
  const count = allLinks.length


  // pick the selection set 
  const linksSelections = info.operation.selectionSet.selections[0].selectionSet.selections[0]
  const queriedLinkes = await ctx.db.query.links({ }, linksSelections)

  return {
    links: queriedLinkes,
    count
  }
}

This works now, but it looks like a bad approach in general. I also think the the pick the selection set line only works for specific queries.

Is there a more elegant solution for transforming an incoming info object before passing it into a resolver? 🙂

Add proxy support

The apollo-link that is created should support a proxy, based on process.env.HTTP_PROXY and process.env.HTTPS_PROXY. Will create a PR for this later.

How to rename query fields when forwarding

Let's say I want to forward the allPosts field to the posts field using forwardTo, is that possible?

A next step would be to also forward the filter input argument to the where input argument.

Syntax for subscription filters

I want to run a subscription using filters:

    publications: {
      subscribe: async(parent, args, ctx, info) => {
        return ctx.db.subscription.post({ where: {
         mutation_in: [CREATED, UPDATED],
          node: {
            isPublished: true
          }
        }}, info)
      },
    },

this doesn't work, and results in this error message when running the subscription:

CREATED is not defined

This one also doesn't work:

    publications: {
      subscribe: async(parent, args, ctx, info) => {
        return ctx.db.subscription.post({ where: {
          mutation_in: "[CREATED, UPDATED]"
          node: {
            isPublished: true
          }
        }}, info)
      },
    },

when I run the subscription, I don't get any response or error message.

What's the correct syntax for subscription filters? Note that I am using JS.

Action required: Greenkeeper could not be activated 🚨

🚨 You need to enable Continuous Integration on all branches of this repository. 🚨

To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because it uses your CI build statuses to figure out when to notify you about breaking changes.

Since we didn’t receive a CI status on the greenkeeper/initial branch, it’s possible that you don’t have CI set up yet. We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.

If you have already set up a CI for this repository, you might need to check how it’s configured. Make sure it is set to run on all new branches. If you don’t want it to run on absolutely every branch, you can whitelist branches starting with greenkeeper/.

Once you have installed and configured CI on this repository correctly, you’ll need to re-trigger Greenkeeper’s initial pull request. To do this, please delete the greenkeeper/initial branch in this repository, and then remove and re-add this repository to the Greenkeeper App’s white list on Github. You'll find this list on your repo or organization’s settings page, under Installed GitHub Apps.

Wrong argument not detected

  const roleIds = ['id1', 'id2'];
  const data = {
    roles: { connect: { id: roleIds } },
  };
  return ctx.db.mutation.createMembership({ data }, info);

returns this error:

[GraphQL error]: Message: No Node for the model Role with value id1,id2 for id found., Location: [object Object], Path: createMembership
Error: No Node for the model Role with value id1,id2 for id found.
    at Object.checkResultAndHandleErrors (/my-app/node_modules/graphql-tools/dist/stitching/errors.js:69:36)
    at Object.<anonymous> (/my-app/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:92:52)
    at step (/my-app/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:40:23)
    at Object.next (/my-app/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:21:53)
    at fulfilled (/my-app/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:12:58)
    at <anonymous>
    at process._tickCallback (internal/process/next_tick.js:188:7)

Note

No Node for the model Role with value id1,id2 for id found.

It looks like graphcool-binding is converting the list of strings that was passed into the id argument to a single string, instead of either throwing an error or passing the list of strings onto the GraphQL API.

see here for more information: https://www.graph.cool/forum/t/connect-mutation-for-many-to-many/2074?u=nilan

Handling errors

How can I catch errors that occur when using graphcool-binding? I want to

  • await the resolver call
  • inspect it, to see if it has errors
  • in case of an error, apply some kind of logic that depends on the specific error. Here I could use the specific error code, or some other information to decide what I do. I could try it again with different input values, return a human readable error, or send an email.

When an error happens currently, it is directly returned to the end-user.


Now follows a description to easily enforce an error:

Adjust the writePost mutation from typescript-basic (see https://github.com/graphql-cli/graphql-boilerplate/tree/master/typescript-basic) to:

  async writePost(parent, { title, text }, ctx: Context, info) {
    const authorId = getUserId(ctx)
    return ctx.db.mutation.createPost(
      {
        data: {
          title,
          text,
-         isPublished: true,
+         isPublishe: true,
          author: {
            connect: { id: authorId },
          },
        },
      },
      info,
    )
  },

Now, running

mutation a {
  writePost(
    title: "Nilan"
    text: "Test"
  ) {
    id
  }
}

results in

{
  "data": null,
  "errors": [
    {
      "message": "Variable \"$_data\" got invalid value {\"title\":\"Nilan\",\"text\":\"Test\",\"isPublishe\":true,\"author\":{\"connect\":{\"id\":\"cjb9m533i001c01787c2zyhqg\"}}}.\nIn field \"isPublishe\": Unknown field.\nIn field \"isPublished\": Expected \"Boolean!\", found null.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "writePost"
      ]
    }
  ]
}

and the server prints

GraphQLError: Variable "$_data" got invalid value {"title":"Nilan","text":"Test","isPublishe":true,"author":{"connect":{"id":"cjb9m533i001c01787c2zyhqg"}}}.
In field "isPublishe": Unknown field.
In field "isPublished": Expected "Boolean!", found null.
    at Object.locatedError (/Users/marktani/projects/playground/permissions-binding/node_modules/graphql/error/locatedError.js:23:10)
    at Object.checkResultAndHandleErrors (/Users/marktani/projects/playground/permissions-binding/node_modules/graphql-tools/src/stitching/errors.ts:83:11)
    at Object.<anonymous> (/Users/marktani/projects/playground/permissions-binding/node_modules/graphql-tools/src/stitching/delegateToSchema.ts:91:14)
    at step (/Users/marktani/projects/playground/permissions-binding/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:40:23)
    at Object.next (/Users/marktani/projects/playground/permissions-binding/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:21:53)
    at fulfilled (/Users/marktani/projects/playground/permissions-binding/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:12:58)
    at <anonymous>
    at process._tickDomainCallback (internal/process/next_tick.js:228:7)

module initialization error - subscriptions-transport-ws - Amazon lambda

I have written a lambda function which is using prisma-binding to create new instances of a type Snippet

Prisma.mutation.createSnippet({ 
data: {
    title: article.title,
    featuredImage: article.urlToImage,
    summary: article.description,
    publishedAt: article.publishedAt,
    url: article.url,
    source: {
        connect: { slug: article.source.id }
   }
}})

The instance is setup using the following:

import { Prisma } from '../generated/prisma'

export default new Prisma({
    endpoint: process.env.PRISMA_ENDPOINT,
    debug: false,//(process.env.NODE_ENV !== 'production'),
    secret: process.env.PRISMA_SECRET
})

The result when executing via a GET request to the lambda url or when using a scheduler event is:

module initialization error: Error
at Error (native)
at Object.fs.openSync (fs.js:641:18)
at Object.fs.readFileSync (fs.js:509:33)
at Object.Module._extensions..js (module.js:578:20)
at Module.load (module.js:487:32)
at tryModuleLoad (module.js:446:12)
at Function.Module._load (module.js:438:3)
at Module.require (module.js:497:17)
at require (internal/module.js:20:19)
at Object.<anonymous> (/var/task/node_modules/subscriptions-transport-ws/dist/client.js:19:27)
at Module._compile (module.js:570:32)
at Object.Module._extensions..js (module.js:579:10)
at Module.load (module.js:487:32)
at tryModuleLoad (module.js:446:12)
at Function.Module._load (module.js:438:3)
at Module.require (module.js:497:17)

I don't believe this is an intended result.

Properties from nested mutations not included in response

I expect the nested mutations to be included in the response of a call to a Prisma.mutations.create* type of function when no info argument is presented. For example, consider the following prisma data model:

type Company {
  id: ID! @unique
  name: String!
  users: [User!]!
}

type User {
  id: ID! @unique
  name: String!
  email: String! @unique
  password: String!
  company: Company!
}

And the following schema:

type Mutation {
  signupCompany(companyName: String!, userName: String!, userEmail: String!, password: String!): AuthPayload!
}

type AuthPayload {
  token: String!
  user: User!
}

I've defined this resolver using prisma-binding injected on its context as property 'db':

export async function signupCompany(parent, args, ctx: Context, info) {
  const { companyName, adminName, adminEmail, password } = args;
  const hashedPassword = await hashPassword(password);
  const user = await ctx.db.mutation.createUser({
    data: {
      name: adminName,
      email: adminEmail,
      password: hashedPassword,
      company: {
        create: { name: companyName },
      },
    },
  });
  return {
    user,
    token: getToken(user),
  };
}

But the response of ctx.db.mutation.createUser(...) doesn't include the company created in the nested mutation, so if I try to do this:

mutation {
  signupCompany(companyName: "JustSocks", adminName: "Gabriel Cangussu", adminEmail: "[email protected]", password: "123password") {
    token
    user {
      id
      name
      email
      company {
        id
        name
      }
    }
  }
}

I get the error Cannot return null for non-nullable field User.company. I also have this same issue on my login mutation that also returns and AuthPayload.

Is there any elegant way of telling ctx.db.mutation.createUser() to return the company if the original graphql mutation needs it? Right now I've hard coded a string to the second argument of createUser() that includes all properties and sub properties of User, but this is not very maintainable as the schema of users and companies can change.

Same field cannot have new resolver/type.

# datamodel.graphql
type Post {
  id: ID! @unique
  createdAt: DateTime!
  updatedAt: DateTime!

  likes: [Like!]!
}
# schema.graphql
type Post {
  id: ID! @unique
  createdAt: DateTime!
  updatedAt: DateTime!

  likes: PostLikes!
}

type PostLikes {
  count: Int!
  liked_by_user: Boolean!
}
// Post.ts
export const Post = {
   likes: ({id}) => resolver
}

Query:

query Post {
   post(id: "cjclyohk3000h0192boujndcv") {
      id
      caption
      likes {
         liked_by_user
         count
      }
   }
}

The following query throws an error (type Like under likes should have selected sub-fields). I assume this happens because likes part of the query gets badly passed down to the binding, which then throws an error. Changing the name from likes to post_likes, for example, solves the problem, but I think it should work with the same name as well.

Feature Request: Codegen'd resolvers

First, amazing job and I'm one of your biggest fans! I hand coded a GraphQL server with user accounts and auth. Never again, especially when there's GraphCool!

One of the biggest GraphQL pain points is the sheer amount of boilerplate. I enjoyed not writing resolvers with GraphCool. The 1.0 version requires a server, which I love, but it brings back some resolver boilerplate. I would love to see some method of automatically generating resolvers from the CRUD API. Perhaps from the CLI?

e.g.

graphcool magically-add-user-resolver-for-create --at index.js and in my index.js I find:

 Mutation: {
   ...
    createUser(parent, { name }, ctx, info) {
      return ctx.db.mutation.createUser(
        { data: { name } },
        info,
      )
    },

Thank you and keep up the amazing work!

Better logging/debugging

For example, sometimes you run into an error message like this:

GraphQLError: Response not successful: Received status code 500
    at Object.locatedError (/Users/marcusboehm/R/github.com/graphcool/cloud-api/node_modules/graphql/error/locatedError.js:23:10)
    at Object.checkResultAndHandleErrors (/Users/marcusboehm/R/github.com/graphcool/cloud-api/node_modules/graphql-tools/src/stitching/errors.ts:83:11)
    at Object.<anonymous> (/Users/marcusboehm/R/github.com/graphcool/cloud-api/node_modules/graphql-tools/src/stitching/delegateToSchema.ts:91:14)
    at step (/Users/marcusboehm/R/github.com/graphcool/cloud-api/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:40:23)
    at Object.next (/Users/marcusboehm/R/github.com/graphcool/cloud-api/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:21:53)
    at fulfilled (/Users/marcusboehm/R/github.com/graphcool/cloud-api/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:12:58)
    at <anonymous>
    at process._tickDomainCallback (internal/process/next_tick.js:228:7)

There should be a flag to enable better logging:

  • more information when an error occurs
  • more information when mutations/queries run successfully
  • ...

Using alias on a request that uses forwardTo("db") returns incorrect data

Requests

query feed($authorId: ID!) {
  posts {
    video {
      posts {
        id
      }
      a: posts(where: {author: {id: $authorId}}) {
        id
      }
    }
  }
}

When running a graphql-yoga server that forwards the requests on to a prisma-cluster, the graphql-yoga server returns this response to the above request:

{
  "data": {
    "posts": [
      {
        "video": {
          "posts": [
            {
              "id": "cjcz88b3o00220171myc5ug63"
            }
          ],
          "a": [
            {
              "id": "cjcz88b3o00220171myc5ug63"
            }
          ]
        }
      }
    ]
  }
}

when running the same request directly on prisma cluster this is returned.

{
  "data": {
    "posts": [
      {
        "video": {
          "posts": [
            {
              "id": "cjcz88b3o00220171myc5ug63"
            }
          ],
          "a": []
        }
      }
    ]
  }
}

The correct result should be the one returned from the prisma cluster ( just above). I think the problem is that the fields are renamed to match the alias twice. Once by the some code in or below prisma-binding and then again in graphql-yoga.

filtering subscriptions `withFilter`

building on issue #78. withFilter seems to need some handholding to work as I would expect. withFilter requires a resolved AsyncIterator, but ctx.db.subscription.link returns a Promise - so you have to await. Finally I was surprised that I have to invoke the function filteredSubscription before the return - it feels like this is different than the usual examples that leverage PubSub - where the function itself is returned.

Can somebody help explain why this works, and if the syntax is expected?

  subscribe: async (parent, args, ctx, info) => {
    const subscription = await ctx.db.subscription.link(
      { },
      info,
    )

    filteredSubscription =  withFilter( () => subscription , (payload, variables) => {
      return true
    })

    return filteredSubscription()
  } 

support configurable subscriptions endpoint

Instead of hardcoding wss://subscriptions.graph.cool/v1/${serviceId} in GraphcoolLink, it should be passed as a param in the root Graphcool ctor which internally passes it to GraphcoolLink ctor. This'll help in testing subscriptions locally using docker

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.