├── .vscode ├── tasks.json ├── settings.json └── launch.json ├── .eslintignore ├── .yo-rc.json ├── src ├── index.ts ├── interfaces.ts ├── boot.ts ├── methods.ts ├── utils.ts ├── typedefs.ts ├── execution.ts ├── resolvers.ts └── ast.ts ├── common ├── models │ ├── googlemaps.js │ ├── catalogs.js │ ├── products.js │ ├── reader.json │ ├── order.js │ ├── account.js │ ├── address.js │ ├── customer.js │ ├── email-address.js │ ├── account.json │ ├── email-address.json │ ├── book.json │ ├── address.json │ ├── order.json │ ├── note.js │ ├── link.json │ ├── author.js │ ├── catalogs.json │ ├── products.json │ ├── customer.json │ ├── author.json │ ├── note.json │ └── googlemaps.json └── types │ └── content.json ├── client └── README.md ├── resources └── loopback-graphql.png ├── .npmignore ├── server ├── component-config.json ├── middleware.development.json ├── datasources.json ├── boot │ ├── root.js │ └── authentication.js ├── config.json ├── middleware.json ├── server.js ├── model-config.json └── data.json ├── tsconfig.test.json ├── CONTRIBUTING.md ├── tsconfig.release.json ├── .editorconfig ├── __test__ ├── testHelper.ts ├── pagination.spec.ts ├── query.spec.ts ├── mutation.spec.ts └── data.json ├── .gitignore ├── jsconfig.json ├── .travis.yml ├── tsconfig.json ├── LICENSE ├── .jsbeautifyrc ├── data.json ├── README.md ├── tslint.json ├── package.json ├── .eslintrc.json └── CHANGELOG.md /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /client/* 2 | /coverage/** 3 | /out/** -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-loopback": {} 3 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { boot } from './boot'; 2 | module.exports = boot; 3 | -------------------------------------------------------------------------------- /common/models/googlemaps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(Googlemaps) {}; -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | ## Client 2 | 3 | This is the place for your application front-end files. 4 | -------------------------------------------------------------------------------- /common/models/catalogs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(Catalogs) { 4 | 5 | }; -------------------------------------------------------------------------------- /common/models/products.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(Products) { 4 | 5 | }; -------------------------------------------------------------------------------- /resources/loopback-graphql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tallyb/loopback-graphql/HEAD/resources/loopback-graphql.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | server 2 | client 3 | test 4 | coverage 5 | resources 6 | common 7 | data.json 8 | .vscode 9 | .github 10 | ./data.json 11 | build 12 | -------------------------------------------------------------------------------- /server/component-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "loopback-component-explorer": { 3 | "mountPath": "/explorer" 4 | }, 5 | "../build/index.js": {} 6 | } 7 | -------------------------------------------------------------------------------- /server/middleware.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "final:after": { 3 | "strong-error-handler": { 4 | "params": { 5 | "debug": true, 6 | "log": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "moduleResolution": "node", 6 | "inlineSourceMap": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "search.exclude": { 4 | "out": true, 5 | "lib": true 6 | }, 7 | "vsicons.presets.angular": false 8 | } 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Guidelines for contributors 2 | 3 | If you're unsure whether your pull request or feature is something that has a chance of being merged, just open an issue and ask away! 4 | 5 | 6 | ## New contributors 7 | 8 | Are Welcome (no guidelines yet...) -------------------------------------------------------------------------------- /server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory", 5 | "file": "./data.json" 6 | }, 7 | "transient": { 8 | "name": "transient", 9 | "connector": "memory" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /common/models/reader.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Reader", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "properties": { 6 | "name": { 7 | "type": "string" 8 | } 9 | }, 10 | "validations": [], 11 | "relations": {}, 12 | "acls": [], 13 | "methods": {} 14 | } 15 | -------------------------------------------------------------------------------- /server/boot/root.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-relations 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 'use strict'; 6 | 7 | module.exports = function(app) { 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /common/models/order.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-relations 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 'use strict'; 6 | 7 | module.exports = function(Order) { 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /common/models/account.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-relations 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 'use strict'; 6 | 7 | module.exports = function(Account) { 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /common/models/address.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-relations 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 'use strict'; 6 | 7 | module.exports = function(Address) { 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /common/models/customer.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-relations 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 'use strict'; 6 | 7 | module.exports = function(Customer) { 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /common/models/email-address.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-relations 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 'use strict'; 6 | 7 | module.exports = function(EmailAddress) { 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "declaration": false, 6 | "outDir": "build", 7 | "removeComments": true, 8 | "watch": false 9 | }, 10 | "include": [ 11 | "src/**/*" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /common/models/account.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Account", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "properties": { 6 | "name": { 7 | "type": "string" 8 | }, 9 | "balance": { 10 | "type": "number" 11 | } 12 | }, 13 | "validations": [], 14 | "relations": {}, 15 | "acls": [], 16 | "methods": {} 17 | } 18 | -------------------------------------------------------------------------------- /common/models/email-address.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "EmailAddress", 3 | "base": "Model", 4 | "idInjection": true, 5 | "properties": { 6 | "label": { 7 | "type": "string" 8 | }, 9 | "address": { 10 | "type": "string" 11 | } 12 | }, 13 | "validations": [], 14 | "relations": {}, 15 | "acls": [], 16 | "methods": {} 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /__test__/testHelper.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import request from 'supertest'; 4 | import app from '../server/server.js'; 5 | 6 | export function gqlRequest(query: any, status: number, variables?: object) { 7 | 8 | return request(app) 9 | .post('/graphql') 10 | .send({ 11 | query, 12 | variables, 13 | }) 14 | .expect(status); 15 | } 16 | -------------------------------------------------------------------------------- /server/boot/authentication.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: loopback-example-relations 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 'use strict'; 6 | 7 | module.exports = function enableAuthentication(server) { 8 | // enable authentication 9 | }; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependencies 7 | node_modules/ 8 | 9 | # Coverage 10 | coverage 11 | 12 | # Transpiled files 13 | build/ 14 | 15 | # VS Code 16 | #.vscode 17 | #!.vscode/tasks.js 18 | 19 | # JetBrains IDEs 20 | .idea/ 21 | 22 | # Optional npm cache directory 23 | .npm 24 | 25 | # Optional eslint cache 26 | .eslintcache 27 | 28 | # Misc 29 | .DS_Store 30 | data 31 | 32 | package-lock.json 33 | -------------------------------------------------------------------------------- /common/models/book.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Book", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "properties": { 6 | "name": { 7 | "type": "string" 8 | } 9 | }, 10 | "validations": [], 11 | "relations": { 12 | "people": { 13 | "type": "embedsMany", 14 | "model": "Link", 15 | "scope": { 16 | "include": "linked" 17 | } 18 | } 19 | }, 20 | "acls": [], 21 | "methods": {} 22 | } 23 | -------------------------------------------------------------------------------- /common/models/address.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Address", 3 | "base": "Model", 4 | "idInjection": true, 5 | "properties": { 6 | "street": { 7 | "type": "string" 8 | }, 9 | "city": { 10 | "type": "string" 11 | }, 12 | "state": { 13 | "type": "string" 14 | }, 15 | "zipCode": { 16 | "type": "string" 17 | } 18 | }, 19 | "validations": [], 20 | "relations": {}, 21 | "acls": [], 22 | "methods": {} 23 | } 24 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=759670 3 | // for the documentation about the jsconfig.json format 4 | "compilerOptions": { 5 | "target": "es6", 6 | "module": "commonjs", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "exclude": [ 10 | "node_modules", 11 | "bower_components", 12 | "jspm_packages", 13 | "tmp", 14 | "temp" 15 | ], 16 | "env": { 17 | "node": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 3000, 5 | "remoting": { 6 | "context": false, 7 | "rest": { 8 | "normalizeHttpPath": false, 9 | "xml": false 10 | }, 11 | "json": { 12 | "strict": false, 13 | "limit": "100kb" 14 | }, 15 | "urlencoded": { 16 | "extended": true, 17 | "limit": "100kb" 18 | }, 19 | "cors": false, 20 | "handleErrors": false 21 | }, 22 | "legacyExplorer": false 23 | } 24 | -------------------------------------------------------------------------------- /common/models/order.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Order", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": { 9 | "date": { 10 | "type": "date" 11 | }, 12 | "description": { 13 | "type": "string" 14 | } 15 | }, 16 | "validations": [], 17 | "relations": { 18 | "customer": { 19 | "type": "hasOne", 20 | "model": "Customer", 21 | "foreignKey": "id" 22 | } 23 | }, 24 | "acls": [], 25 | "methods": {} 26 | } 27 | -------------------------------------------------------------------------------- /common/types/content.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Content", 3 | "plural": "Content", 4 | "base": "Model", 5 | "idInjection": false, 6 | "options": { 7 | "validateUpsert": true 8 | }, 9 | "properties": { 10 | "title": { 11 | "type": "string" 12 | }, 13 | "body": { 14 | "type": "string" 15 | }, 16 | "footer": { 17 | "type": "string" 18 | } 19 | }, 20 | "validations": [], 21 | "relations": {}, 22 | "acls": [], 23 | "methods": {} 24 | 25 | } -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | //export declare function Model(arg?: { hooks?: {}, remotes?: {} }): any; 2 | 3 | export interface IProperty { 4 | type: any; 5 | deprecated?: Boolean; 6 | required?: Boolean; 7 | defaultFn?: any; 8 | enum?: any; 9 | } 10 | 11 | export interface IField { 12 | list?: Boolean; 13 | scalar?: Boolean; 14 | required?: Boolean; 15 | gqlType: string; 16 | relation?: any; 17 | args?: any; 18 | } 19 | 20 | export interface ITypesHash { 21 | [id: string]: any; 22 | } 23 | 24 | export interface ISchemaType { 25 | category: string; 26 | fields: IField[]; 27 | input: any; 28 | values: any; 29 | } 30 | -------------------------------------------------------------------------------- /common/models/note.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(Note) { 4 | 5 | Note.clear = () => { 6 | return { 7 | note: { 8 | Content: '' 9 | }, 10 | previousClear: new Date() 11 | }; 12 | }; 13 | 14 | Note.remoteMethod( 15 | 'clear', { 16 | 'http': { 17 | 'path': '/clear', 18 | 'verb': 'post' 19 | }, 20 | 'returns': [{ 21 | 'arg': 'note', 22 | 'type': 'object' 23 | }, { 24 | 'arg': 'previousClear', 25 | 'type': 'Date' 26 | }] 27 | }); 28 | }; -------------------------------------------------------------------------------- /common/models/link.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Link", 3 | "base": "Model", 4 | "idInjection": true, 5 | "properties": { 6 | "id": { 7 | "type": "number", 8 | "id": true 9 | }, 10 | "name": { 11 | "type": "string" 12 | }, 13 | "notes": { 14 | "type": "string" 15 | } 16 | }, 17 | "validations": [], 18 | "relations": { 19 | "linked": { 20 | "type": "belongsTo", 21 | "polymorphic": { 22 | "idType": "number" 23 | }, 24 | "properties": { 25 | "name": "name" 26 | }, 27 | "options": { 28 | "invertProperties": true 29 | } 30 | } 31 | }, 32 | "acls": [], 33 | "methods": {} 34 | } 35 | -------------------------------------------------------------------------------- /server/middleware.json: -------------------------------------------------------------------------------- 1 | { 2 | "initial:before": { 3 | "loopback#favicon": {} 4 | }, 5 | "initial": { 6 | "compression": {}, 7 | "cors": { 8 | "params": { 9 | "origin": true, 10 | "credentials": true, 11 | "maxAge": 86400 12 | } 13 | } 14 | }, 15 | "session": { 16 | }, 17 | "auth": { 18 | }, 19 | "parse": { 20 | }, 21 | "routes:before": { 22 | "loopback#rest": { 23 | "paths": ["${restApiRoot}"] 24 | } 25 | }, 26 | "files": { 27 | "serve-static": { 28 | "params": "$!../client" 29 | } 30 | }, 31 | "final": { 32 | "loopback#urlNotFound": {} 33 | }, 34 | "final:after": { 35 | "strong-error-handler": {} 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "7" 5 | 6 | cache: 7 | directories: 8 | - ${HOME}/.npm 9 | 10 | before_install: 11 | - npm config set spin=false 12 | - npm install -g npm@4 13 | - npm install -g coveralls 14 | 15 | install: 16 | - npm install 17 | 18 | script: 19 | - npm test 20 | - npm run coverage 21 | 22 | after_script: 23 | - coveralls < ./coverage/lcov.info || true # if coveralls doesn't have it covered 24 | 25 | branches: 26 | only: 27 | - master 28 | 29 | # Allow Travis tests to run in containers. 30 | sudo: false 31 | 32 | deploy: 33 | provider: npm 34 | email: tally.barak@gmail.com 35 | api_key: $NPM_API_KEY 36 | on: 37 | tags: true 38 | all_branches: true 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noImplicitAny": false, 7 | "allowSyntheticDefaultImports": true, 8 | "allowJs": true, 9 | "importHelpers": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "declaration": false, 17 | "outDir": "build", 18 | "sourceMap": true, 19 | "watch": true 20 | }, 21 | "include": [ 22 | "src/**/*", 23 | "__tests__/**/*" 24 | ], 25 | "typeRoots": [ 26 | "node_modules/@types" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /common/models/author.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(Author) { 4 | 5 | Author.remoteMethod( 6 | 'addFriend', { 7 | 'http': { 8 | 'path': '/addFriend', 9 | 'verb': 'post' 10 | }, 11 | 12 | 'accepts': [{ 13 | 'arg': 'author', 14 | 'type': 'number' 15 | }, { 16 | 'arg': 'friend', 17 | 'type': ['number'] 18 | }], 19 | 20 | 'returns': { 21 | 'arg': 'result', 22 | 'type': 'object' 23 | } 24 | } 25 | ); 26 | 27 | Author.addFriend = function(author, friend) { 28 | 29 | return Author.findById(author) 30 | .then(res => { 31 | let updated = res; 32 | updated.friendIds.push(friend); 33 | return updated.save(); 34 | }).then(res => {}); 35 | }; 36 | }; -------------------------------------------------------------------------------- /src/boot.ts: -------------------------------------------------------------------------------- 1 | import { graphqlExpress, graphiqlExpress } from 'graphql-server-express'; 2 | import { makeExecutableSchema } from 'graphql-tools'; 3 | import * as bodyParser from 'body-parser'; 4 | 5 | import { abstractTypes } from './ast'; 6 | import { resolvers } from './resolvers'; 7 | import { generateTypeDefs } from './typedefs'; 8 | 9 | export function boot(app, options) { 10 | const models = app.models(); 11 | let types = abstractTypes(models); 12 | let schema = makeExecutableSchema({ 13 | typeDefs: generateTypeDefs(types), 14 | resolvers: resolvers(models), 15 | resolverValidationOptions: { 16 | requireResolversForAllFields: false, 17 | }, 18 | }); 19 | 20 | let graphiqlPath = options.graphiqlPath || '/graphiql'; 21 | let path = options.path || '/graphql'; 22 | 23 | app.use(path, bodyParser.json(), graphqlExpress(req => { 24 | return { 25 | schema, 26 | context: req, 27 | }; 28 | })); 29 | app.use(graphiqlPath, graphiqlExpress({ 30 | endpointURL: path, 31 | })); 32 | } 33 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var loopback = require('loopback'); 4 | var boot = require('loopback-boot'); 5 | 6 | var app = module.exports = loopback(); 7 | 8 | app.start = function() { 9 | // start the web server 10 | return app.listen(function() { 11 | app.emit('started'); 12 | var baseUrl = app.get('url').replace(/\/$/, ''); 13 | console.log('Web server listening at: %s', baseUrl); 14 | if (app.get('loopback-component-explorer')) { 15 | var explorerPath = app.get('loopback-component-explorer').mountPath; 16 | console.log('Browse your REST API at %s%s', baseUrl, explorerPath); 17 | } 18 | }); 19 | }; 20 | 21 | // Bootstrap the application, configure models, datasources and middleware. 22 | // Sub-apps like REST API are mounted via boot scripts. 23 | boot(app, __dirname, function(err) { 24 | if (err) { 25 | throw err; 26 | } 27 | 28 | // start the server if `$ node server.js` 29 | if (require.main === module) { 30 | app.start(); 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /common/models/catalogs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "catalogs", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": { 9 | "_id": { 10 | "type": "string", 11 | "generated": true, 12 | "id": true 13 | } 14 | }, 15 | "validations": [], 16 | "relations": { 17 | "products": { 18 | "type": "hasMany", 19 | "model": "products", 20 | "foreignKey": "catalogRef" 21 | } 22 | }, 23 | "acls": [{ 24 | "accessType": "*", 25 | "principalType": "ROLE", 26 | "principalId": "$everyone", 27 | "permission": "DENY" 28 | }, { 29 | "accessType": "*", 30 | "principalType": "ROLE", 31 | "principalId": "$authenticated", 32 | "permission": "ALLOW" 33 | }], 34 | "indexes": { 35 | "catalogRef_1": { 36 | "catalogRef": 1 37 | } 38 | }, 39 | "methods": [], 40 | "permissions": "private", 41 | "mixins": {} 42 | } -------------------------------------------------------------------------------- /common/models/products.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "products", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": { 9 | "_id": { 10 | "type": "string", 11 | "generated": true, 12 | "id": true 13 | } 14 | }, 15 | "validations": [], 16 | "relations": { 17 | "catalog": { 18 | "type": "belongsTo", 19 | "model": "catalogs", 20 | "foreignKey": "catalogRef" 21 | } 22 | }, 23 | "acls": [{ 24 | "accessType": "*", 25 | "principalType": "ROLE", 26 | "principalId": "$everyone", 27 | "permission": "DENY" 28 | }, { 29 | "accessType": "*", 30 | "principalType": "ROLE", 31 | "principalId": "$authenticated", 32 | "permission": "ALLOW" 33 | }], 34 | "indexes": { 35 | "catalogRef_1": { 36 | "catalogRef": 1 37 | } 38 | }, 39 | "methods": [], 40 | "permissions": "private", 41 | "mixins": {} 42 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Tally Barak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /common/models/customer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Customer", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "properties": { 6 | "name": { 7 | "type": "string" 8 | }, 9 | "age": { 10 | "type": "number" 11 | } 12 | }, 13 | "validations": [], 14 | "relations": { 15 | "address": { 16 | "type": "embedsOne", 17 | "model": "Address", 18 | "property": "billingAddress", 19 | "options": { 20 | "validate": true, 21 | "forceId": false 22 | } 23 | }, 24 | "emails": { 25 | "type": "embedsMany", 26 | "model": "EmailAddress", 27 | "property": "emailList", 28 | "options": { 29 | "validate": true, 30 | "forceId": false 31 | } 32 | }, 33 | "accounts": { 34 | "type": "referencesMany", 35 | "model": "Account", 36 | "foreignKey": "accountIds", 37 | "options": { 38 | "validate": true, 39 | "forceId": false 40 | } 41 | }, 42 | "orders": { 43 | "type": "hasMany", 44 | "model": "Order", 45 | "foreignKey": "id" 46 | } 47 | }, 48 | "acls": [], 49 | "methods": {} 50 | } 51 | -------------------------------------------------------------------------------- /common/models/author.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Author", 3 | "plural": "Authors", 4 | "base": "PersistedModel", 5 | "idInjection": true, 6 | "options": { 7 | "validateUpsert": true 8 | }, 9 | "properties": { 10 | "first_name": { 11 | "type": "string", 12 | "required": true 13 | }, 14 | "last_name": { 15 | "type": "string", 16 | "required": true 17 | }, 18 | 19 | "birth_date": { 20 | "type": "date", 21 | "required": true 22 | }, 23 | "user": { 24 | "type": "User", 25 | "required": false 26 | }, 27 | "dream": "Object" 28 | }, 29 | "validations": [], 30 | "relations": { 31 | "notes": { 32 | "type": "hasMany", 33 | "model": "Note", 34 | "foreignKey": "" 35 | }, 36 | "friends": { 37 | "type": "referencesMany", 38 | "model": "Author", 39 | "foreignKey": "friendIds" 40 | }, 41 | "others": { 42 | "type": "hasMany", 43 | "model": "Author" 44 | } 45 | }, 46 | "acls": [], 47 | "methods": {} 48 | } -------------------------------------------------------------------------------- /common/models/note.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Note", 3 | "properties": { 4 | "title": { 5 | "type": "string", 6 | "required": true 7 | }, 8 | "content": { 9 | "type": "Content" 10 | }, 11 | "Genre": { 12 | "type": "string", 13 | "required": false, 14 | "enum": [ 15 | "HUMOR", 16 | "SCI_FI", 17 | "HORROR", 18 | "ROMANCE", 19 | "NON_FICTION" 20 | ] 21 | }, 22 | "location": { 23 | "type": "GeoPoint" 24 | } 25 | }, 26 | "validations": [], 27 | "relations": { 28 | "author": { 29 | "type": "belongsTo", 30 | "model": "Author", 31 | "foreignKey": "" 32 | } 33 | }, 34 | "acls": [], 35 | "methods": { 36 | "prototype.vote": { 37 | "accepts": [{ 38 | "arg": "rank", 39 | "type": "number", 40 | "required": true, 41 | "description": "" 42 | }], 43 | "returns": [], 44 | "description": " vote a note", 45 | "http": [{ 46 | "path": "vote", 47 | "verb": "post" 48 | }] 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "html": { 3 | "allowed_file_extensions": ["htm", "html", "xhtml", "shtml", "xml", "svg"], 4 | "brace_style": "collapse", 5 | "indent_char": " ", 6 | "indent_handlebars": false, 7 | "indent_inner_html": false, 8 | "indent_scripts": "keep", 9 | "indent_size": 4, 10 | "max_preserve_newlines": 1, 11 | "preserve_newlines": true, 12 | "unformatted": ["a", "sub", "sup", "b", "i", "u", "pre"], 13 | "wrap_line_length": 0 14 | }, 15 | "css": { 16 | "allowed_file_extensions": ["css", "scss", "sass", "less"], 17 | "end_with_newline": false, 18 | "indent_char": " ", 19 | "indent_size": 4, 20 | "selector_separator": " ", 21 | "selector_separator_newline": false 22 | }, 23 | "js": { 24 | "allowed_file_extensions": ["js", "json", "jshintrc", "jsbeautifyrc"], 25 | "brace_style": "collapse", 26 | "break_chained_methods": false, 27 | "e4x": true, 28 | "eval_code": false, 29 | "indent_char": " ", 30 | "indent_level": 0, 31 | "indent_size": 4, 32 | "indent_with_tabs": false, 33 | "jslint_happy": false, 34 | "keep_array_indentation": false, 35 | "keep_function_indentation": false, 36 | "max_preserve_newlines": 2, 37 | "preserve_newlines": true, 38 | "space_before_conditional": true, 39 | "space_in_paren": false, 40 | "unescape_strings": false, 41 | "wrap_line_length": 0 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /data.json: -------------------------------------------------------------------------------- 1 | { 2 | "ids": { 3 | "User": 1, 4 | "AccessToken": 1, 5 | "ACL": 1, 6 | "RoleMapping": 1, 7 | "Role": 1, 8 | "Customer": 1, 9 | "Account": 1, 10 | "Author": 6, 11 | "Reader": 1, 12 | "Book": 1, 13 | "Order": 1, 14 | "Note": 1, 15 | "Googlemaps": 1, 16 | "products": 1, 17 | "catalogs": 1 18 | }, 19 | "models": { 20 | "User": {}, 21 | "AccessToken": {}, 22 | "ACL": {}, 23 | "RoleMapping": {}, 24 | "Role": {}, 25 | "Customer": {}, 26 | "Account": {}, 27 | "Author": { 28 | "1": "{\"first_name\":\"Virginia\",\"last_name\":\"Wolf\",\"birth_date\":\"2017-06-30T07:47:24.747Z\",\"friendIds\":[],\"id\":1}", 29 | "2": "{\"first_name\":\"Virginia\",\"last_name\":\"Wolf\",\"birth_date\":\"2017-06-30T07:49:23.893Z\",\"friendIds\":[],\"id\":2}", 30 | "3": "{\"first_name\":\"Virginia\",\"last_name\":\"Wolf\",\"birth_date\":\"2017-06-30T09:43:37.721Z\",\"friendIds\":[],\"id\":3}", 31 | "4": "{\"first_name\":\"Virginia\",\"last_name\":\"Wolf\",\"birth_date\":\"2017-07-08T05:38:45.056Z\",\"friendIds\":[],\"id\":4}", 32 | "5": "{\"first_name\":\"Virginia\",\"last_name\":\"Wolf\",\"birth_date\":\"2017-09-23T04:09:57.126Z\",\"friendIds\":[],\"id\":5}" 33 | }, 34 | "Reader": {}, 35 | "Book": {}, 36 | "Order": {}, 37 | "Note": {}, 38 | "Googlemaps": {}, 39 | "products": {}, 40 | "catalogs": {} 41 | } 42 | } -------------------------------------------------------------------------------- /src/methods.ts: -------------------------------------------------------------------------------- 1 | // import * as _ from 'lodash'; 2 | // import {toType} from './ast'; 3 | // import {} from './utils'; 4 | // import {Property} from './interfaces'; 5 | 6 | 7 | // function generateAccepts(name: string, props: Property[]) { 8 | // let ret = _.map(props, prop => { 9 | // let propType = prop.type; 10 | // if (_.isArray(prop.type)) { 11 | // propType = prop.type[0]; 12 | // } 13 | // return propType ? `${prop.arg}: [${toType(prop.type[0])}]${prop.required ? '!' : ''}` : ''; 14 | // }).join(' \n '); 15 | // return ret ? `(${ret})` : ''; 16 | 17 | // } 18 | 19 | // function generateReturns(name, props) { 20 | // if (_.isObject(props)) { 21 | // props = [props]; 22 | // } 23 | // let args; 24 | // args = _.map(props, prop => { 25 | // if (_.isArray(prop.type)) { 26 | // return `${prop.arg}: [${toType(prop.type[0])}]${prop.required ? '!' : ''}`; 27 | // } else if (toType(prop.type)) { 28 | // return `${prop.arg}: ${toType(prop.type)}${prop.required ? '!' : ''}`; 29 | // } 30 | // return ''; 31 | // }).join(' \n '); 32 | // return args ? `{${args}}` : ''; 33 | // } 34 | 35 | // export default function generateMethods(model) { 36 | // return _.chain(model.sharedClass.methods()) 37 | // .map(method => { 38 | // if (method.accessType === 'WRITE' && method.http.path) { 39 | // return `${utils.methodName(method)} 40 | // ${generateAccepts(method.name, method.accepts)} 41 | // ${generateReturns(method.name, method.returns)} 42 | // : JSON`; 43 | // } else { 44 | // return undefined; 45 | // } 46 | // }) 47 | // .compact() 48 | // .value() 49 | // .join(' \n '); 50 | // } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Deprecation Warning: 3 | I have been very bad at maintaining this repo, and do not see how I come back to it in the near future. 4 | I have seen that this repo is a fork that moved forward: https://github.com/yahoohung/loopback-graphql-server/. 5 | If anyone is willing to take maintaining this repo on themselves - you are welcome. 6 | Thanks for all the feedback... 7 | 8 | ### Status 9 | [![Build Status](https://travis-ci.org/Tallyb/loopback-graphql.svg?branch=master)](https://travis-ci.org/Tallyb/loopback-graphql) 10 | 11 | # GraphQL Server for Loopback (Apollo Server) 12 | 13 | Combine the powers of [ApolloStack](http://www.apollostack.com/) GraphQL with the backend of Loopback. 14 |
15 | All of Loopback models are exposed as GraphQL Queries. 16 |
17 | Define models in Loopback to be exposed as REST APIs and GraphQL queries and mutations *. 18 |
19 | Use the Apollo [clients](http://dev.apollodata.com/) to access your data. 20 | 21 | ![Loopback Graphql](./resources/loopback-graphql.png?raw=true "LoopBack Apollo Architecture") 22 | 23 | ## Getting started 24 | 25 | ```sh 26 | npm install loopback-graphql 27 | ``` 28 | Add the loopback-graphql component to the `server/component-config.json`: 29 | 30 | ``` 31 | "loopback-graphql": { 32 | "path": "/graphql", 33 | "graphiqlPath":"/graphiql" 34 | } 35 | ``` 36 | 37 | Requests will be posted to `path` path. (Default: `/graphql`); 38 | 39 | Graphiql is available on `graphiqlPath` path. (Default: `/graphiql`); 40 | 41 | ## Usage 42 | 43 | Access the Graphiql interface to view your GraphQL model on the Docs section. 44 | Build the GraphQL queries and use them in your application. 45 | 46 | geoPoint objects are supported as follow: 47 | ``` 48 | {"newNote": 49 | { 50 | "location": {"lat":40.77492964101182, "lng":-73.90950187151662} 51 | } 52 | } 53 | ``` 54 | 55 | ## Roadmap 56 | [See here the Github project](https://github.com/Tallyb/loopback-graphql/projects/1) 57 | -------------------------------------------------------------------------------- /server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "../common/types", 8 | "./models" 9 | ] 10 | }, 11 | "User": { 12 | "dataSource": "db" 13 | }, 14 | "AccessToken": { 15 | "dataSource": "db", 16 | "public": false 17 | }, 18 | "ACL": { 19 | "dataSource": "db", 20 | "public": false 21 | }, 22 | "RoleMapping": { 23 | "dataSource": "db", 24 | "public": false 25 | }, 26 | "Role": { 27 | "dataSource": "db", 28 | "public": false 29 | }, 30 | "Customer": { 31 | "dataSource": "db", 32 | "public": true 33 | }, 34 | "Address": { 35 | "dataSource": "transient", 36 | "public": false 37 | }, 38 | "EmailAddress": { 39 | "dataSource": "transient", 40 | "public": false 41 | }, 42 | "Account": { 43 | "dataSource": "db", 44 | "public": false 45 | }, 46 | "Author": { 47 | "dataSource": "db", 48 | "public": true 49 | }, 50 | "Reader": { 51 | "dataSource": "db", 52 | "public": false 53 | }, 54 | "Book": { 55 | "dataSource": "db", 56 | "public": true 57 | }, 58 | "Link": { 59 | "dataSource": "transient", 60 | "public": false 61 | }, 62 | "Order": { 63 | "dataSource": "db", 64 | "public": true 65 | }, 66 | "Note": { 67 | "dataSource": "db", 68 | "public": true 69 | }, 70 | "Content": { 71 | "dataSource": null, 72 | "public": false 73 | }, 74 | "Googlemaps": { 75 | "dataSource": "db", 76 | "public": true 77 | }, 78 | "products": { 79 | "dataSource": "db", 80 | "public": true 81 | }, 82 | "catalogs": { 83 | "dataSource": "db", 84 | "public": true 85 | } 86 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | const PAGINATION = '(where: JSON, after: String, first: Int, before: String, last: Int)'; 4 | 5 | function base64(i) { 6 | return (new Buffer(i, 'ascii')).toString('base64'); 7 | } 8 | 9 | function unbase64(i) { 10 | return (new Buffer(i, 'base64')).toString('ascii'); 11 | } 12 | 13 | const PREFIX = 'connection.'; 14 | 15 | /** 16 | * Creates the cursor string from an offset. 17 | * @param {String} id the id to convert 18 | * @returns {String} an opaque cursor 19 | */ 20 | function idToCursor(id) { 21 | return base64(PREFIX + id); 22 | } 23 | 24 | /** 25 | * Rederives the offset from the cursor string. 26 | * @param {String} cursor the cursor for conversion 27 | * @returns {String} id converted id 28 | */ 29 | function cursorToId(cursor) { 30 | return unbase64(cursor).substring(PREFIX.length); 31 | } 32 | 33 | function getId(cursor) { 34 | if (cursor === undefined || cursor === null) { 35 | return null; 36 | } 37 | return cursorToId(cursor); 38 | } 39 | 40 | function connectionTypeName(model) { 41 | return `${model.modelName}Connection`; 42 | } 43 | 44 | function edgeTypeName(model: any) { 45 | return `${model.modelName}Edge`; // e.g. UserEdge 46 | } 47 | 48 | function singularModelName(model) { 49 | return model.modelName; 50 | } 51 | 52 | function pluralModelName(model: any) { 53 | return 'all' + _.upperFirst(model.pluralModelName); 54 | } 55 | 56 | function sharedRelations(model: any) { 57 | return _.pickBy(model.relations, rel => rel.modelTo && rel.modelTo.shared); 58 | } 59 | 60 | function sharedModels(models: any[]) { 61 | return _.filter(models, model => { 62 | return model.shared; 63 | }); 64 | } 65 | 66 | function methodName(method, model) { 67 | return model.modelName + _.upperFirst(method.name); 68 | } 69 | export { 70 | PAGINATION, 71 | getId, 72 | idToCursor, 73 | cursorToId, 74 | connectionTypeName, 75 | edgeTypeName, 76 | singularModelName, 77 | methodName, 78 | pluralModelName, 79 | sharedRelations, 80 | sharedModels, 81 | }; 82 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | 5 | { 6 | "name": "Tests", 7 | "type": "node", 8 | "request": "launch", 9 | "program": "${workspaceRoot}/node_modules/jest-cli/bin/jest.js", 10 | "stopOnEntry": false, 11 | "args": ["--runInBand"], 12 | "cwd": "${workspaceRoot}", 13 | "runtimeExecutable": null, 14 | "runtimeArgs": [ 15 | "--nolazy" 16 | ], 17 | "env": { 18 | "NODE_ENV": "development" 19 | }, 20 | "console": "internalConsole", 21 | "sourceMaps": true 22 | }, 23 | { 24 | "name": "Launch", 25 | "type": "node", 26 | "request": "launch", 27 | "program": "${workspaceRoot}/server/server.js", 28 | "stopOnEntry": false, 29 | "args": [], 30 | "cwd": "${workspaceRoot}", 31 | "preLaunchTask": null, 32 | "outFiles": ["build"], 33 | "runtimeExecutable": null, 34 | "runtimeArgs": [ 35 | "--nolazy" 36 | ], 37 | "env": { 38 | "NODE_ENV": "development" 39 | }, 40 | "console": "internalConsole", 41 | "sourceMaps": true 42 | }, 43 | { 44 | "name": "Attach", 45 | "type": "node", 46 | "request": "attach", 47 | "port": 5858, 48 | "address": "localhost", 49 | "restart": false, 50 | "sourceMaps": false, 51 | "outFiles": ["dist"], 52 | "localRoot": "${workspaceRoot}", 53 | "remoteRoot": null 54 | }, 55 | { 56 | "name": "Attach to Process", 57 | "type": "node", 58 | "request": "attach", 59 | "processId": "${command:PickProcess}", 60 | "port": 5858, 61 | "sourceMaps": false, 62 | "outFiles": ["dist"] 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /src/typedefs.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import { ISchemaType, IField, ITypesHash } from './interfaces'; 3 | 4 | const scalarTypes = ` 5 | scalar Date 6 | scalar JSON 7 | scalar GeoPoint 8 | `; 9 | 10 | function args(params: string): string { 11 | return params ? `(${args})` : ''; 12 | } 13 | 14 | function generateInputField(field: IField, name: string): string { 15 | return ` 16 | ${name} : ${field.list ? '[' : ''} 17 | ${field.gqlType}${field.scalar ? '' : 'Input'}${field.required ? '!' : ''} ${field.list ? ']' : ''}`; 18 | } 19 | 20 | function generateOutputField(field: IField, name: string): string { 21 | return `${name} ${args(field.args)} : ${field.list ? '[' : ''}${field.gqlType}${field.required ? '!' : ''} ${field.list ? ']' : ''}`; 22 | } 23 | 24 | export function generateTypeDefs(types: ITypesHash) { 25 | const categories = { 26 | TYPE: (type: ISchemaType, name: string) => { 27 | let output = _.reduce(type.fields, (res: string, field: IField, fieldName: string): string => { 28 | return res + generateOutputField(field, fieldName) + ' \n '; 29 | }, ''); 30 | 31 | let result = ` 32 | type ${name} { 33 | ${output} 34 | }`; 35 | if (type.input) { 36 | let input = _.reduce(type.fields, (accumulator: string, field: IField, fieldName: string) => { 37 | return !field.relation ? accumulator + generateInputField(field, fieldName) + ' \n ' : accumulator; 38 | }, ''); 39 | result += `input ${name}Input { 40 | ${input} 41 | }`; 42 | } 43 | return result; 44 | }, 45 | UNION: (type: ISchemaType, name: string) => { 46 | return `union ${name} = ${type.values.join(' | ')}`; 47 | }, 48 | ENUM: (type: ISchemaType, name: string) => { 49 | return `enum ${name} {${type.values.join(' ')}}`; 50 | }, 51 | }; 52 | 53 | return _.reduce(types, (result: string, type: ISchemaType, name: string) => { 54 | return result + categories[type.category](type, name); 55 | }, scalarTypes); 56 | } 57 | -------------------------------------------------------------------------------- /__test__/pagination.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { gqlRequest } from './testHelper'; 3 | import gql from 'graphql-tag'; 4 | // var _ = require('lodash'); 5 | 6 | describe('Pagination', () => { 7 | 8 | 9 | it('should query first 2 entities', () => { 10 | const query = gql`{ 11 | allNotes(first: 2) { 12 | totalCount 13 | pageInfo { 14 | hasNextPage 15 | hasPreviousPage 16 | startCursor 17 | endCursor 18 | } 19 | edges { 20 | node { 21 | title 22 | id 23 | } 24 | cursor 25 | } 26 | } 27 | } 28 | 29 | `; 30 | return gqlRequest(query, 200) 31 | .then(res => { 32 | let data: any = res.body.data; 33 | expect(data.allNotes.edges.length).toBeGreaterThan(0); 34 | }); 35 | }); 36 | 37 | it('should query entity after cursor', () => { 38 | const query = gql`{ 39 | allNotes (after: "Y29ubmVjdGlvbi40", first: 3) { 40 | pageInfo { 41 | hasNextPage 42 | hasPreviousPage 43 | startCursor 44 | endCursor 45 | } 46 | edges { 47 | node { 48 | id 49 | title 50 | } 51 | cursor 52 | } 53 | } 54 | }`; 55 | return gqlRequest(query, 200) 56 | .then(res => { 57 | let data: any = res.body.data; 58 | expect(data.allNotes.edges.length).toBeGreaterThan(0); 59 | expect(data.allNotes.edges[0].node.id).toBeGreaterThan(4); 60 | expect(data.allNotes.pageInfo.hasPreviousPage).toEqual(true); 61 | }); 62 | }); 63 | 64 | it('should query related entity on edge', () => { 65 | const query = gql`{ 66 | allAuthors { 67 | pageInfo { 68 | hasNextPage 69 | hasPreviousPage 70 | startCursor 71 | endCursor 72 | } 73 | edges { 74 | node { 75 | id 76 | last_name 77 | notes { 78 | totalCount 79 | Notes { 80 | title 81 | } 82 | } 83 | } 84 | cursor 85 | } 86 | } 87 | } 88 | `; 89 | return gqlRequest(query, 200) 90 | .then(res => { 91 | let data: any = res.body.data; 92 | expect(data.allAuthors.edges[0].node.notes.Notes.length).toBeGreaterThan(0); 93 | expect(data.allAuthors.edges[0].node.notes.totalCount).toBeGreaterThan(0); 94 | //data.allAuthors.edges[0].cursor.should.not.to.be.empty(); 95 | }); 96 | }); 97 | 98 | }); 99 | -------------------------------------------------------------------------------- /__test__/query.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { gqlRequest } from './testHelper'; 3 | import gql from 'graphql-tag'; 4 | 5 | describe('query', () => { 6 | 7 | describe('Single entity', () => { 8 | it('should execute a single query with relation', () => { 9 | const query = gql` 10 | query { 11 | allOrders(first:1){ 12 | edges{ 13 | node{ 14 | date 15 | description 16 | customer{ 17 | edges{ 18 | node{ 19 | name 20 | age 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | }`; 28 | return gqlRequest(query, 200, {}) 29 | .then(res => { 30 | console.log('RES', res.body.data); 31 | let data = res.body.data; 32 | expect(data.allOrders.edges.length).toEqual(1); 33 | }); 34 | }); 35 | }); 36 | 37 | describe('Multiple entities', () => { 38 | it('should return response with where on id', () => { 39 | const query = gql` 40 | query users ($where:JSON){ 41 | allUsers(where: $where) { 42 | totalCount 43 | edges { 44 | node { 45 | id 46 | email 47 | } 48 | } 49 | 50 | } 51 | }`; 52 | const variables = { 53 | where: { 54 | id: { 55 | inq: [1, 2], 56 | }, 57 | }, 58 | }; 59 | return gqlRequest(query, 200, variables) 60 | .then(res => { 61 | expect(res.body.data.allUsers.totalCount).toEqual(2); 62 | }); 63 | 64 | }); 65 | }); 66 | 67 | describe('relationships', () => { 68 | it('should query related entity with nested relational data', () => { 69 | const query = gql` 70 | query { 71 | allCustomers(first:2){ 72 | edges{ 73 | node{ 74 | name 75 | age 76 | orders{ 77 | edges{ 78 | node{ 79 | date 80 | description 81 | customer{ 82 | edges{ 83 | node{ 84 | name 85 | age 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | `; 97 | return gqlRequest(query, 200, {}) 98 | .then(res => { 99 | expect(res.body.data.allCustomers.edges.length).toEqual(2); 100 | }); 101 | }); 102 | }); 103 | 104 | }); 105 | -------------------------------------------------------------------------------- /__test__/mutation.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { gqlRequest } from './testHelper'; 3 | import gql from 'graphql-tag'; 4 | // var _ = require('lodash'); 5 | 6 | describe('mutation', () => { 7 | 8 | it('should add and Delete single entity', () => { 9 | let id; 10 | const createAuthor = gql` 11 | mutation save ($obj: AuthorInput!) { 12 | saveAuthor (obj: $obj) { 13 | first_name 14 | last_name 15 | birth_date 16 | id 17 | } 18 | } 19 | `; 20 | const authorInput = { 21 | first_name: 'Virginia', 22 | last_name: 'Wolf', 23 | birth_date: new Date(), 24 | }; 25 | const deleteAuthor = gql` 26 | mutation delete ($id: ID!) { 27 | deleteAuthor (id: $id) { 28 | text 29 | } 30 | } 31 | `; 32 | 33 | return gqlRequest(createAuthor, 200, { 34 | obj: authorInput, 35 | }) 36 | .then(res => { 37 | id = res.body.data.saveAuthor.id; 38 | return gqlRequest(deleteAuthor, 200, { 39 | id: id, 40 | }); 41 | }); 42 | }); 43 | 44 | it('should add a single entity with sub type', () => { 45 | const body = 'Heckelbery Finn'; 46 | const query = gql` 47 | mutation save ($obj: NoteInput!) { 48 | saveNote (obj: $obj) { 49 | id 50 | title 51 | author { 52 | first_name 53 | last_name 54 | } 55 | 56 | } 57 | } 58 | `; 59 | const variables = { 60 | obj: { 61 | title: 'Heckelbery Finn', 62 | content: { 63 | body: body, 64 | footer: 'The end', 65 | }, 66 | }, 67 | }; 68 | 69 | return gqlRequest(query, 200, variables) 70 | .then(res => { 71 | expect(res.body.data.saveNote.title).toEqual(body); 72 | }); 73 | }); 74 | 75 | describe('remote methods', () => { 76 | 77 | const userInput = { 78 | email: 'John@a.com', 79 | password: '123456', 80 | username: 'John@a.com', 81 | }; 82 | const createUser = ` 83 | mutation userCreate ($obj: UserInput!) { 84 | saveUser ( obj: $obj ) { 85 | id 86 | } 87 | } 88 | `; 89 | const deleteUser = gql` 90 | mutation delete ($id: ID!) { 91 | deleteAuthor (id: $id) { 92 | text 93 | } 94 | } 95 | `; 96 | let userId; 97 | 98 | beforeEach(() => { 99 | return gqlRequest(createUser, 200, { 100 | obj: userInput, 101 | }) 102 | .then(res => { 103 | userId = res.body.data.saveUser.id; 104 | }); 105 | }); 106 | 107 | afterEach(() => { 108 | return gqlRequest(deleteUser, 200, { 109 | id: userId, 110 | }); 111 | }); 112 | it.skip('should login and return an accessToken', () => { 113 | const query = gql` 114 | mutation login{ 115 | UserLogin(credentials:{username:"John@a.com", password:"123456"}) 116 | } 117 | `; 118 | return gqlRequest(query, 200) 119 | .then(res => { 120 | expect(res.body.data.UserLogin).toHaveProperty('id'); 121 | }); 122 | }); 123 | 124 | }); 125 | 126 | }); 127 | -------------------------------------------------------------------------------- /src/execution.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import { 4 | getId, 5 | connectionTypeName, 6 | idToCursor, 7 | } from './utils'; 8 | 9 | function buildSelector(model, args) { 10 | let selector = { 11 | where: args.where || {}, 12 | skip: undefined, 13 | limit: undefined, 14 | order: undefined, 15 | }; 16 | const begin = getId(args.after); 17 | const end = getId(args.before); 18 | 19 | selector.skip = args.first - args.last || 0; 20 | selector.limit = args.last || args.first; 21 | selector.order = model.getIdName() + (end ? ' DESC' : ' ASC'); 22 | if (begin) { 23 | selector.where[model.getIdName()] = selector[model.getIdName()] || {}; 24 | selector.where[model.getIdName()].gt = begin; 25 | } 26 | if (end) { 27 | selector.where[model.getIdName()] = selector[model.getIdName()] || {}; 28 | selector.where[model.getIdName()].lt = end; 29 | } 30 | return selector; 31 | } 32 | 33 | function findOne(model, obj, args /*, context*/) { 34 | let id = obj ? obj[model.getIdName()] : args.id; 35 | return model.findById(id); 36 | } 37 | 38 | function getCount(model, obj, args, context) { 39 | return model.count(args.where, obj, context); 40 | } 41 | 42 | function getFirst(model, obj, args) { 43 | return model.findOne({ 44 | order: model.getIdName() + (args.before ? ' DESC' : ' ASC'), 45 | where: args.where, 46 | }, obj) 47 | .then(res => { 48 | return res ? res.__data : {}; 49 | }); 50 | } 51 | 52 | function getList(model, args) { 53 | return model.find(buildSelector(model, args)); 54 | } 55 | 56 | function findAll(model: any, obj: any, args: any) { 57 | const response = { 58 | args: args, 59 | count: undefined, 60 | first: undefined, 61 | list: undefined, 62 | }; 63 | return getCount(model, obj, args, undefined) 64 | .then(count => { 65 | response.count = count; 66 | return getFirst(model, obj, args); 67 | }) 68 | .then(first => { 69 | response.first = first; 70 | return getList(model, args); 71 | }) 72 | .then(list => { 73 | response.list = list; 74 | return response; 75 | }); 76 | } 77 | 78 | function findRelated(rel, obj, args) { 79 | if (_.isArray(obj[rel.keyFrom])) { 80 | return []; 81 | } 82 | args.where = { 83 | [rel.keyTo]: obj[rel.keyFrom], 84 | }; 85 | return findAll(rel.modelTo, obj, args); 86 | 87 | } 88 | 89 | function resolveConnection(model) { 90 | return { 91 | [connectionTypeName(model)]: { 92 | totalCount: (obj) => { 93 | return obj.count; 94 | }, 95 | 96 | edges: (obj) => { 97 | return _.map(obj.list, node => { 98 | return { 99 | cursor: idToCursor(node[model.getIdName()]), 100 | node: node, 101 | }; 102 | }); 103 | }, 104 | 105 | [model.pluralModelName]: (obj) => { 106 | return obj.list; 107 | }, 108 | 109 | pageInfo: (obj) => { 110 | let pageInfo = { 111 | startCursor: null, 112 | endCursor: null, 113 | hasPreviousPage: false, 114 | hasNextPage: false, 115 | }; 116 | if (obj.count > 0) { 117 | pageInfo.startCursor = idToCursor(obj.list[0][model.getIdName()]); 118 | pageInfo.endCursor = idToCursor(obj.list[obj.list.length - 1][model.getIdName()]); 119 | pageInfo.hasNextPage = obj.list.length === obj.args.limit; 120 | pageInfo.hasPreviousPage = obj.list[0][model.getIdName()] !== obj.first[model.getIdName()].toString(); 121 | } 122 | return pageInfo; 123 | }, 124 | }, 125 | }; 126 | } 127 | 128 | export { 129 | findAll, 130 | findOne, 131 | findRelated, 132 | resolveConnection, 133 | }; 134 | -------------------------------------------------------------------------------- /__test__/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "ids": { 3 | "User": 3, 4 | "AccessToken": 1, 5 | "ACL": 1, 6 | "RoleMapping": 1, 7 | "Role": 1, 8 | "Note": 7, 9 | "Author": 6, 10 | "Reader": 2, 11 | "Book": 2, 12 | "Customer": 8, 13 | "Address": 1, 14 | "EmailAddress": 1, 15 | "Account": 3, 16 | "Order": 6 17 | }, 18 | "models": { 19 | "User": { 20 | "1": "{\"username\":\"tally\",\"password\":\"$2a$10$b/Ok3FC3HYCbF2LlLXpk4OSsrz5EixJfkfJittHp8eEtPQvUJZiEa\",\"email\":\"tally@a.com\",\"id\":1}", 21 | }, 22 | "AccessToken": {}, 23 | "ACL": {}, 24 | "RoleMapping": {}, 25 | "Role": {}, 26 | "Note": { 27 | "1": "{\"title\":\"1st note\",\"id\":1}", 28 | "2": "{\"title\":\"Who is Afraid\",\"Genre\":\"HUMOR\",\"authorId\":3,\"id\":2}", 29 | "3": "{\"title\":\"A room with a View\",\"authorId\":3,\"id\":3}", 30 | "4": "{\"title\":\"The Bluest Eye\",\"authorId\":5,\"id\":4}", 31 | "5": "{\"title\":\"Of Mice and Men\",\"Genre\":\"HUMOR\",\"authorId\":4,\"id\":5}", 32 | "6": "{\"title\":\"Love and Prejudice\",\"authorId\":8,\"Genre\":\"ROMANCE\",\"id\":6}" 33 | }, 34 | "Author": { 35 | "1": "{\"first_name\":\"Jane\",\"last_name\":\"Austin\",\"birth_date\":\"1883-10-15T00:00:00.000Z\",\"id\":8,\"friendIds\":[5,7]}", 36 | "3": "{\"first_name\":\"Virginia\",\"last_name\":\"Wolf\",\"birth_date\":\"2017-06-30T07:36:32.666Z\",\"friendIds\":[],\"id\":3}", 37 | }, 38 | "Reader": { 39 | "1": "{\"name\":\"Reader 1\",\"id\":1}" 40 | }, 41 | "Book": { 42 | "1": "{\"name\":\"Book 1\",\"links\":[{\"id\":1,\"name\":\"Author 1\",\"notes\":\"Note 1\",\"linkedId\":1,\"linkedType\":\"Author\"},{\"id\":2,\"name\":\"Reader 1\",\"notes\":\"Note 2\",\"linkedId\":1,\"linkedType\":\"Reader\"}],\"id\":1}" 43 | }, 44 | "Order": { 45 | "1": "{\"date\":\"2014-12-31T18:30:00.000Z\",\"description\":\"First order by Customer A\",\"customerId\":1,\"id\":1}", 46 | "2": "{\"date\":\"2015-01-31T18:30:00.000Z\",\"description\":\"Second order by Customer A\",\"customerId\":1,\"id\":2}", 47 | "3": "{\"date\":\"2015-02-28T18:30:00.000Z\",\"description\":\"Order by Customer B\",\"customerId\":2,\"id\":3}", 48 | "4": "{\"date\":\"2015-03-31T18:30:00.000Z\",\"description\":\"Order by Customer C\",\"customerId\":3,\"id\":4}", 49 | "5": "{\"date\":\"2015-04-30T18:30:00.000Z\",\"description\":\"Order by Anonymous\",\"id\":5}" 50 | }, 51 | "Customer": { 52 | "1": "{\"name\":\"Customer A\",\"age\":21,\"emailList\":[],\"accountIds\":[],\"id\":1}", 53 | "2": "{\"name\":\"Customer B\",\"age\":22,\"emailList\":[],\"accountIds\":[],\"id\":2}", 54 | "3": "{\"name\":\"Customer C\",\"age\":23,\"emailList\":[],\"accountIds\":[],\"id\":3}", 55 | "4": "{\"name\":\"Customer D\",\"age\":24,\"emailList\":[],\"accountIds\":[],\"id\":4}", 56 | "5": "{\"name\":\"Mary Smith\",\"emailList\":[],\"accountIds\":[1,2],\"id\":5}", 57 | "6": "{\"name\":\"John Smith\",\"emailList\":[],\"accountIds\":[],\"id\":6,\"billingAddress\":{\"street\":\"123 A St\",\"city\":\"San Jose\",\"state\":\"CA\",\"zipCode\":\"95131\"}}", 58 | "7": "{\"name\":\"Larry Smith\",\"emailList\":[{\"label\":\"home\",\"address\":\"larry@yahoo.com\",\"id\":2,\"name\":\"home\"}],\"accountIds\":[],\"id\":7}" 59 | }, 60 | "Address": {}, 61 | "EmailAddress": {}, 62 | "Account": { 63 | "1": "{\"name\":\"Checking\",\"balance\":5000,\"id\":1}", 64 | "2": "{\"name\":\"Saving\",\"balance\":2000,\"id\":2}" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /server/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "ids": { 3 | "User": 3, 4 | "AccessToken": 1, 5 | "ACL": 1, 6 | "RoleMapping": 1, 7 | "Role": 1, 8 | "Note": 7, 9 | "Author": 6, 10 | "Reader": 2, 11 | "Book": 2, 12 | "Customer": 8, 13 | "Address": 1, 14 | "EmailAddress": 1, 15 | "Account": 3, 16 | "Order": 6 17 | }, 18 | "models": { 19 | "User": { 20 | "1": "{\"username\":\"tally\",\"password\":\"$2a$10$b/Ok3FC3HYCbF2LlLXpk4OSsrz5EixJfkfJittHp8eEtPQvUJZiEa\",\"email\":\"tally@a.com\",\"id\":1}", 21 | }, 22 | "AccessToken": {}, 23 | "ACL": {}, 24 | "RoleMapping": {}, 25 | "Role": {}, 26 | "Note": { 27 | "1": "{\"title\":\"1st note\",\"id\":1}", 28 | "2": "{\"title\":\"Who is Afraid\",\"Genre\":\"HUMOR\",\"authorId\":3,\"id\":2}", 29 | "3": "{\"title\":\"A room with a View\",\"authorId\":3,\"id\":3}", 30 | "4": "{\"title\":\"The Bluest Eye\",\"authorId\":5,\"id\":4}", 31 | "5": "{\"title\":\"Of Mice and Men\",\"Genre\":\"HUMOR\",\"authorId\":4,\"id\":5}", 32 | "6": "{\"title\":\"Love and Prejudice\",\"authorId\":8,\"Genre\":\"ROMANCE\",\"id\":6}" 33 | }, 34 | "Author": { 35 | "1": "{\"first_name\":\"Jane\",\"last_name\":\"Austin\",\"birth_date\":\"1883-10-15T00:00:00.000Z\",\"id\":8,\"friendIds\":[5,7]}", 36 | "3": "{\"first_name\":\"Virginia\",\"last_name\":\"Wolf\",\"birth_date\":\"2017-06-30T07:36:32.666Z\",\"friendIds\":[],\"id\":3}", 37 | }, 38 | "Reader": { 39 | "1": "{\"name\":\"Reader 1\",\"id\":1}" 40 | }, 41 | "Book": { 42 | "1": "{\"name\":\"Book 1\",\"links\":[{\"id\":1,\"name\":\"Author 1\",\"notes\":\"Note 1\",\"linkedId\":1,\"linkedType\":\"Author\"},{\"id\":2,\"name\":\"Reader 1\",\"notes\":\"Note 2\",\"linkedId\":1,\"linkedType\":\"Reader\"}],\"id\":1}" 43 | }, 44 | "Order": { 45 | "1": "{\"date\":\"2014-12-31T18:30:00.000Z\",\"description\":\"First order by Customer A\",\"customerId\":1,\"id\":1}", 46 | "2": "{\"date\":\"2015-01-31T18:30:00.000Z\",\"description\":\"Second order by Customer A\",\"customerId\":1,\"id\":2}", 47 | "3": "{\"date\":\"2015-02-28T18:30:00.000Z\",\"description\":\"Order by Customer B\",\"customerId\":2,\"id\":3}", 48 | "4": "{\"date\":\"2015-03-31T18:30:00.000Z\",\"description\":\"Order by Customer C\",\"customerId\":3,\"id\":4}", 49 | "5": "{\"date\":\"2015-04-30T18:30:00.000Z\",\"description\":\"Order by Anonymous\",\"id\":5}" 50 | }, 51 | "Customer": { 52 | "1": "{\"name\":\"Customer A\",\"age\":21,\"emailList\":[],\"accountIds\":[],\"id\":1}", 53 | "2": "{\"name\":\"Customer B\",\"age\":22,\"emailList\":[],\"accountIds\":[],\"id\":2}", 54 | "3": "{\"name\":\"Customer C\",\"age\":23,\"emailList\":[],\"accountIds\":[],\"id\":3}", 55 | "4": "{\"name\":\"Customer D\",\"age\":24,\"emailList\":[],\"accountIds\":[],\"id\":4}", 56 | "5": "{\"name\":\"Mary Smith\",\"emailList\":[],\"accountIds\":[1,2],\"id\":5}", 57 | "6": "{\"name\":\"John Smith\",\"emailList\":[],\"accountIds\":[],\"id\":6,\"billingAddress\":{\"street\":\"123 A St\",\"city\":\"San Jose\",\"state\":\"CA\",\"zipCode\":\"95131\"}}", 58 | "7": "{\"name\":\"Larry Smith\",\"emailList\":[{\"label\":\"home\",\"address\":\"larry@yahoo.com\",\"id\":2,\"name\":\"home\"}],\"accountIds\":[],\"id\":7}" 59 | }, 60 | "Address": {}, 61 | "EmailAddress": {}, 62 | "Account": { 63 | "1": "{\"name\":\"Checking\",\"balance\":5000,\"id\":1}", 64 | "2": "{\"name\":\"Saving\",\"balance\":2000,\"id\":2}" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | false, 5 | "parameters", 6 | "arguments", 7 | "statements" 8 | ], 9 | "ban": false, 10 | "class-name": true, 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "interface-name": [], 19 | "jsdoc-format": true, 20 | "label-position": true, 21 | "max-line-length": [ 22 | true, 23 | 140 24 | ], 25 | "member-access": true, 26 | "member-ordering": [ 27 | true, 28 | "public-before-private", 29 | "static-before-instance", 30 | "variables-before-functions" 31 | ], 32 | "no-any": false, 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-conditional-assignment": true, 36 | "no-consecutive-blank-lines": false, 37 | "no-console": [ 38 | true, 39 | "log", 40 | "debug", 41 | "info", 42 | "time", 43 | "timeEnd", 44 | "trace" 45 | ], 46 | "no-construct": true, 47 | "no-debugger": true, 48 | "no-duplicate-variable": true, 49 | "no-empty": true, 50 | "no-eval": true, 51 | "no-inferrable-types": [], 52 | "no-internal-module": true, 53 | "no-null-keyword": false, 54 | "no-require-imports": false, 55 | "no-shadowed-variable": true, 56 | "no-switch-case-fall-through": true, 57 | "no-trailing-whitespace": true, 58 | "no-unused-expression": true, 59 | "no-var-keyword": true, 60 | "no-var-requires": true, 61 | "object-literal-sort-keys": false, 62 | "one-line": [ 63 | true, 64 | "check-open-brace", 65 | "check-catch", 66 | "check-else", 67 | "check-finally", 68 | "check-whitespace" 69 | ], 70 | "quotemark": [ 71 | true, 72 | "single", 73 | "avoid-escape" 74 | ], 75 | "radix": true, 76 | "semicolon": [ 77 | true, 78 | "always" 79 | ], 80 | "switch-default": true, 81 | "trailing-comma": [ 82 | true, 83 | { 84 | "multiline": "always", 85 | "singleline": "never" 86 | } 87 | ], 88 | "triple-equals": [ 89 | true, 90 | "allow-null-check" 91 | ], 92 | "typedef": [ 93 | false, 94 | "call-signature", 95 | "parameter", 96 | "arrow-parameter", 97 | "property-declaration", 98 | "variable-declaration", 99 | "member-variable-declaration" 100 | ], 101 | "typedef-whitespace": [ 102 | true, 103 | { 104 | "call-signature": "nospace", 105 | "index-signature": "nospace", 106 | "parameter": "nospace", 107 | "property-declaration": "nospace", 108 | "variable-declaration": "nospace" 109 | }, 110 | { 111 | "call-signature": "space", 112 | "index-signature": "space", 113 | "parameter": "space", 114 | "property-declaration": "space", 115 | "variable-declaration": "space" 116 | } 117 | ], 118 | "variable-name": [ 119 | true, 120 | "check-format", 121 | "allow-leading-underscore", 122 | "ban-keywords" 123 | ], 124 | "whitespace": [ 125 | true, 126 | "check-branch", 127 | "check-decl", 128 | "check-operator", 129 | "check-separator", 130 | "check-type" 131 | ] 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-graphql", 3 | "version": "0.13.0", 4 | "description": "Add Apollo Server or GraphQL queries on your Loopback server", 5 | "main": "build/src/index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "types": "build/index", 10 | "scripts": { 11 | "clean": "rimraf coverage build tmp", 12 | "build": "tsc -p tsconfig.release.json", 13 | "lint": "tslint -t stylish '{src,__tests__}/**/*.{ts,tsx}'", 14 | "pretest": "npm run lint && npm run copydata", 15 | "test": "npm run test:only", 16 | "test:only": "jest --coverage", 17 | "test:watch": "jest --watch", 18 | "copydata": "cpx ./__test__/data.json ./server/", 19 | "test:notify": "npm run test:watch -- --notify", 20 | "test:co": "npm run test -- --runInBand", 21 | "coverage": "npm test -- --coverage", 22 | "coverage:notify": "npm run coverage -- --watch --notify", 23 | "start": "npm run build && node server/server.js", 24 | "start:watch": "concurrently \"npm run build:watch\" \"node-dev server/server.js\"", 25 | "prerelease": "npm test", 26 | "release": "standard-version" 27 | }, 28 | "repository": { 29 | "url": "git+https://github.com/tallyb/loopback-graphql.git", 30 | "type": "git" 31 | }, 32 | "keywords": [ 33 | "Loopback", 34 | "GraphQL", 35 | "Apollo", 36 | "Express", 37 | "Javascript", 38 | "REST", 39 | "APIs" 40 | ], 41 | "author": "Tally Barak ", 42 | "license": "MIT", 43 | "bugs": { 44 | "url": "https://github.com/tallyb/loopback-graphql/issues" 45 | }, 46 | "homepage": "https://github.com/tallyb/loopback-graphql#readme", 47 | "dependencies": { 48 | "body-parser": "^1.17.2", 49 | "graphql": "^0.12.0", 50 | "graphql-date": "^1.0.3", 51 | "graphql-geojson": "^1.0.0", 52 | "graphql-server-express": "^1.0.0", 53 | "graphql-tools": "^3.0.0", 54 | "graphql-type-json": "^0.2.0", 55 | "lodash": "^4.17.4" 56 | }, 57 | "devDependencies": { 58 | "@types/jest": "22.0.0", 59 | "@types/graphql": "^0.13.4", 60 | "@types/jest": "^21.1.0", 61 | "@types/lodash": "^4.14.66", 62 | "@types/request": "2.0.7", 63 | "@types/node": "^10.5.5", 64 | "@types/request-promise": "^4.1.33", 65 | "@types/supertest": "^2.0.1", 66 | "@types/uuid": "^3.0.0", 67 | "awesome-typescript-loader": "^5.0.0", 68 | "compression": "^1.6.2", 69 | "concurrently": "^3.4.0", 70 | "cors": "^2.8.3", 71 | "cpx": "^1.5.0", 72 | "eslint": "^5.0.0", 73 | "express": "^4.15.3", 74 | "ghooks": "^2.0.0", 75 | "graphql-tag": "^2.3.0", 76 | "helmet": "^3.6.1", 77 | "jest-cli": "^22.0.0", 78 | "jest": "^22.0.0", 79 | "loopback": "^3.8.0", 80 | "loopback-boot": "^2.24.1", 81 | "loopback-component-explorer": "^6.0.0", 82 | "loopback-datasource-juggler": "^3.9.1", 83 | "node-dev": "^3.1.3", 84 | "nsp": "^3.0.0", 85 | "request": "^2.81.0", 86 | "rimraf": "^2.6.1", 87 | "serve-favicon": "^2.4.3", 88 | "standard-version": "^4.2.0", 89 | "strong-error-handler": "^3.0.0", 90 | "supertest": "^3.0.0", 91 | "ts-jest": "^23.1.0", 92 | "tslint": "^5.4.3", 93 | "typescript": "^2.4.0" 94 | }, 95 | "jest": { 96 | "transform": { 97 | ".(ts|tsx)": "/node_modules/ts-jest/preprocessor.js" 98 | }, 99 | "mapCoverage": true, 100 | "testEnvironment": "node", 101 | "testRegex": ".*\\.spec\\.ts$", 102 | "moduleFileExtensions": [ 103 | "ts", 104 | "js", 105 | "json" 106 | ], 107 | "globals": { 108 | "__DEV__": true, 109 | "ts-jest": { 110 | "tsConfigFile": "tsconfig.test.json" 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/resolvers.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import * as utils from './utils'; 3 | 4 | import * as execution from './execution'; 5 | import * as GraphQLJSON from 'graphql-type-json'; 6 | import * as GraphQLDate from 'graphql-date'; 7 | import { 8 | CoordinatesScalar, 9 | } from 'graphql-geojson'; 10 | 11 | const scalarResolvers = { 12 | JSON: GraphQLJSON, 13 | Date: GraphQLDate, 14 | GeoPoint: CoordinatesScalar, 15 | }; 16 | 17 | function RelationResolver(model) { 18 | let resolver = {}; 19 | _.forEach(utils.sharedRelations(model), rel => { 20 | resolver[rel.name] = (obj, args) => { 21 | return execution.findRelated(rel, obj, args); 22 | }; 23 | }); 24 | 25 | return { 26 | [utils.singularModelName(model)]: resolver, 27 | }; 28 | } 29 | 30 | function rootResolver(model) { 31 | return { 32 | Query: { 33 | [`${utils.pluralModelName(model)}`]: (root, args) => { 34 | return execution.findAll(model, root, args); 35 | }, 36 | [`${utils.singularModelName(model)}`]: (obj, args) => { 37 | return execution.findOne(model, obj, args); 38 | }, 39 | }, 40 | Mutation: { 41 | [`save${utils.singularModelName(model)}`]: (_root, args) => { 42 | return model.upsert(args.obj); 43 | }, 44 | [`delete${utils.singularModelName(model)}`]: (_root, args) => { 45 | return model.findById(args.id) 46 | .then(instance => { 47 | return instance ? instance.destroy() : null; 48 | }); 49 | }, 50 | }, 51 | }; 52 | } 53 | 54 | function connectionResolver(model: any) { 55 | return { 56 | [utils.connectionTypeName(model)]: { 57 | totalCount: (obj) => { 58 | return obj.count; 59 | }, 60 | 61 | edges: (obj) => { 62 | return _.map(obj.list, node => { 63 | return { 64 | cursor: utils.idToCursor(node[model.getIdName()]), 65 | node: node, 66 | }; 67 | }); 68 | }, 69 | 70 | [model.pluralModelName]: (obj) => { 71 | return obj.list; 72 | }, 73 | 74 | pageInfo: (obj) => { 75 | let pageInfo = { 76 | startCursor: null, 77 | endCursor: null, 78 | hasPreviousPage: false, 79 | hasNextPage: false, 80 | }; 81 | if (obj.count > 0) { 82 | pageInfo.startCursor = utils.idToCursor(obj.list[0][model.getIdName()]); 83 | pageInfo.endCursor = utils.idToCursor(obj.list[obj.list.length - 1][model.getIdName()]); 84 | pageInfo.hasNextPage = obj.list.length === obj.args.limit; 85 | pageInfo.hasPreviousPage = obj.list[0][model.getIdName()] !== obj.first[model.getIdName()].toString(); 86 | } 87 | return pageInfo; 88 | }, 89 | }, 90 | }; 91 | } 92 | 93 | function remoteResolver(model) { 94 | let mutation = {}; 95 | //model.sharedClass.methods 96 | if (model.sharedClass && model.sharedClass.methods) { 97 | model.sharedClass._methods.map(function (method) { 98 | if (method.accessType !== 'READ' && method.http.path) { 99 | let acceptingParams = []; 100 | method.accepts.map(function (param) { 101 | if (param.arg) { 102 | acceptingParams.push(param.arg); 103 | } 104 | }); 105 | mutation[`${utils.methodName(method, model)}`] = (args) => { 106 | let params = []; 107 | _.each(method.accepts, (el, i) => { 108 | params[i] = args[el.arg]; 109 | }); 110 | return model[method.name].apply(model, params); 111 | }; 112 | } 113 | }); 114 | } 115 | return { 116 | Mutation: mutation, 117 | }; 118 | } 119 | 120 | /** 121 | * Generate resolvers for all models 122 | * 123 | * @param {Object} models: All loopback Models 124 | * @returns {Object} resolvers functions for all models - queries and mutations 125 | */ 126 | export function resolvers(models: any[]) { 127 | return _.reduce(models, (obj: any, model: any) => { 128 | if (model.shared) { 129 | return _.merge( 130 | obj, 131 | rootResolver(model), 132 | connectionResolver(model), 133 | RelationResolver(model), 134 | remoteResolver(model), 135 | ); 136 | } 137 | return obj; 138 | }, scalarResolvers); 139 | } 140 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "amd": true, 5 | "browser": true, 6 | "mocha": true, 7 | "es6": true, 8 | "jasmine": true, 9 | "node": true 10 | }, 11 | "globals": { 12 | "assert": true, 13 | "browser": true, 14 | "expect": true, 15 | "by": true, 16 | "protractor": true, 17 | "sinon": true, 18 | "xdescribe": true, 19 | "xit": true 20 | }, 21 | "plugins": [ 22 | "nodeca" 23 | ], 24 | "rules": { 25 | "comma-dangle": [1, "ignore"], 26 | "camelcase": 0, 27 | "complexity": [0, 11], 28 | "consistent-return": 2, 29 | "consistent-this": [1, "self"], 30 | "curly": [2, "all"], 31 | "dot-notation": 2, 32 | "eol-last": 0, 33 | "eqeqeq": [2, "smart"], 34 | "indent": [0, 1, { 35 | "SwitchCase": 1 36 | }], 37 | "keyword-spacing": [2, { 38 | "before": true, 39 | "after": true, 40 | "overrides": {} 41 | }], 42 | "nodeca/indent": [2, "spaces", 4], 43 | "max-depth": [0, 4], 44 | "max-len": [0, 80, 4], 45 | "max-nested-callbacks": [0, 2], 46 | "max-params": [0, 15], 47 | "max-statements": [0, 10], 48 | "new-cap": [0], 49 | "new-parens": 2, 50 | "no-alert": 2, 51 | "no-array-constructor": 2, 52 | "no-caller": 2, 53 | "no-catch-shadow": 2, 54 | "no-cond-assign": 2, 55 | "no-console": 1, 56 | "no-constant-condition": 2, 57 | "no-control-regex": 2, 58 | "no-debugger": 2, 59 | "no-delete-var": 2, 60 | "no-dupe-keys": 2, 61 | "no-empty": 0, 62 | "no-empty-character-class": 2, 63 | "no-labels": 2, 64 | "no-ex-assign": 2, 65 | "no-extend-native": 2, 66 | "no-extra-boolean-cast": 2, 67 | "no-extra-parens": 1, 68 | "no-extra-semi": 2, 69 | "no-fallthrough": 2, 70 | "no-func-assign": 2, 71 | "no-implied-eval": 2, 72 | "no-invalid-regexp": 2, 73 | "no-iterator": 2, 74 | "no-label-var": 2, 75 | "no-lone-blocks": 2, 76 | "no-lonely-if": 0, 77 | "no-loop-func": 2, 78 | "no-mixed-spaces-and-tabs": [2, true], 79 | "no-multi-str": 2, 80 | "no-multiple-empty-lines": [2, { 81 | "max": 1 82 | }], 83 | "no-native-reassign": 2, 84 | "no-negated-in-lhs": 2, 85 | "no-nested-ternary": 0, 86 | "no-new": 2, 87 | "no-new-func": 2, 88 | "no-new-object": 2, 89 | "no-new-require": 0, 90 | "no-new-wrappers": 2, 91 | "no-obj-calls": 2, 92 | "no-octal": 2, 93 | "no-octal-escape": 2, 94 | "no-path-concat": 2, 95 | "no-process-exit": 1, 96 | "no-proto": 2, 97 | "no-redeclare": 2, 98 | "no-regex-spaces": 2, 99 | "no-return-assign": 2, 100 | "no-script-url": 2, 101 | "no-sequences": 2, 102 | "no-shadow": 0, 103 | "no-shadow-restricted-names": 2, 104 | "no-spaced-func": 2, 105 | "no-sparse-arrays": 2, 106 | "no-sync": 0, 107 | "no-ternary": 0, 108 | "no-trailing-spaces": 2, 109 | "no-undef": 2, 110 | "no-undef-init": 2, 111 | "no-underscore-dangle": 0, 112 | "no-unreachable": 2, 113 | "no-unused-vars": [2, { 114 | "args": "none", 115 | "vars": "local" 116 | }], 117 | "no-use-before-define": 2, 118 | "no-warning-comments": [1, { 119 | "terms": ["todo", "fixme", "xxx"], 120 | "location": "start" 121 | }], 122 | "no-with": 2, 123 | "quotes": [2, "single"], 124 | "semi": 2, 125 | "semi-spacing": 2, 126 | "space-infix-ops": [2, { 127 | "int32Hint": false 128 | }], 129 | "strict": [2, "global"], 130 | "use-isnan": 2, 131 | "valid-jsdoc": [2, { 132 | "prefer": { 133 | "return": "returns" 134 | }, 135 | "requireReturn": false, 136 | "requireParamDescription": true 137 | }], 138 | "valid-typeof": 2, 139 | "wrap-iife": [2, "any"], 140 | "yoda": [2, "never", { 141 | "exceptRange": true 142 | }] 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /common/models/googlemaps.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Googlemaps", 3 | "plural": "Googlemaps", 4 | "base": "PersistedModel", 5 | "idInjection": false, 6 | "options": { 7 | "validateUpsert": true 8 | }, 9 | "properties": { 10 | "id": { 11 | "type": "string", 12 | "description": "contains a unique stable identifier denoting this place. This identifier may not be used to retrieve information about this place, but is guaranteed to be valid across sessions. It can be used to consolidate data about this place, and to verify the identity of a place across separate searches. **Note**: The `id` is now deprecated in favor of `place_id`." 13 | }, 14 | "geometry": { 15 | "type": { 16 | "location": { 17 | "lat": "number", 18 | "lng": "number" 19 | }, 20 | "viewport": { 21 | "northeast": { 22 | "lat": "number", 23 | "lng": "number" 24 | }, 25 | "southwest": { 26 | "lat": "number", 27 | "lng": "number" 28 | } 29 | } 30 | }, 31 | "description": "contains geometry information about the result, generally including the `location` (geocode) of the place and (optionally) the `viewport` identifying its general area of coverage." 32 | }, 33 | "photos": { 34 | "description": "an array of `photo` objects, each containing a reference to an image. A Place Search will return at most one `photo` object. Performing a Place Details request on the place may return up to ten photos. More information about Place Photos and how you can use the images in your application can be found in the [Place Photos](https://developers.google.com/places/web-service/photos) documentation.", 35 | "type": [{ 36 | "photo_reference": { 37 | "type": "string", 38 | "description": " a string used to identify the photo when you perform a Photo request." 39 | }, 40 | "height": { 41 | "type": "number", 42 | "description": " the maximum height of the image." 43 | }, 44 | "width": { 45 | "type": "number", 46 | "description": " the maximum width of the image." 47 | }, 48 | "html_attributions": { 49 | "type": ["string"], 50 | "description": "contains any required attributions. This field will always be present, but may be empty." 51 | } 52 | }] 53 | }, 54 | "scope": { 55 | "type": "string", 56 | "description": [ 57 | "Indicates the scope of the `place_id`. The possible values are:", " * `APP`: The place ID is recognised by your application only. This is because your application added the place, and the place has not yet passed the moderation process.", " * `GOOGLE`: The place ID is available to other applications and on Google Maps.", "**Note**: The `scope` field is included only in Nearby Search results and Place Details results. You can only retrieve app-scoped places via the Nearby Search and the Place Details requests. If the `scope` field is not present in a response, it is safe to assume the scope is `GOOGLE`." 58 | ] 59 | }, 60 | "alt_ids": { 61 | "type": { 62 | "place_id": { 63 | "type": "string", 64 | "description": "The most likely reason for a place to have an alternative place ID is if your application adds a place and receives an application-scoped place ID, then later receives a Google-scoped place ID after passing the moderation process." 65 | }, 66 | "scope": { 67 | "type": "string", 68 | "description": "The scope of an alternative place ID will always be APP, indicating that the alternative place ID is recognised by your application only." 69 | } 70 | }, 71 | "description": ["An array of zero, one or more alternative place IDs for the place, with a scope related to each alternative ID. Note: This array may be empty or not present.", "For example, let's assume your application adds a place and receives a `place_id` of `AAA` for the new place. Later, the place passes the moderation process and receives a Google-scoped `place_id` of `BBB`. From this point on, the information for this place will contain:", "```", "\"results\" : [", " {", " \"place_id\" : \"BBB\",", " \"scope\" : \"GOOGLE\",", " \"alt_ids\" : [", " {", " \"place_id\" : \"AAA\",", " \"scope\" : \"APP\",", " }", " ],", " }", " ]", "```"] 72 | } 73 | }, 74 | "validations": [], 75 | "relations": {}, 76 | "acls": [], 77 | "methods": {} 78 | } 79 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | # [0.13.0](https://github.com/tallyb/loopback-graphql/compare/v0.12.1...v0.13.0) (2017-05-19) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * **app:** Packages upgrades ([c7cf2de](https://github.com/tallyb/loopback-graphql/commit/c7cf2de)) 12 | * **resolvers:** Remove workaround. Closes [#44](https://github.com/tallyb/loopback-graphql/issues/44) ([758ebbf](https://github.com/tallyb/loopback-graphql/commit/758ebbf)) 13 | 14 | 15 | 16 | 17 | ## [0.12.1](https://github.com/tallyb/loopback-graphql/compare/v0.12.0...v0.12.1) (2017-04-30) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * **github:** Remove github template ([353aadf](https://github.com/tallyb/loopback-graphql/commit/353aadf)) 23 | * **package:** update graphql-server-express to version 0.7.0 ([#49](https://github.com/tallyb/loopback-graphql/issues/49)) ([36cf7b4](https://github.com/tallyb/loopback-graphql/commit/36cf7b4)) 24 | 25 | 26 | 27 | 28 | # [0.12.0](https://github.com/tallyb/loopback-graphql/compare/v0.11.0...v0.12.0) (2017-04-01) 29 | 30 | 31 | ### Features 32 | 33 | * **app:** Remove scalar types and use libs ([749b5ee](https://github.com/tallyb/loopback-graphql/commit/749b5ee)) 34 | 35 | 36 | 37 | 38 | # [0.11.0](https://github.com/tallyb/loopback-graphql/compare/v0.10.0...v0.11.0) (2017-03-31) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * **package:** update graphql-server-express to version 0.5.0 ([#24](https://github.com/tallyb/loopback-graphql/issues/24)) ([8f84b54](https://github.com/tallyb/loopback-graphql/commit/8f84b54)) 44 | * **package:** update graphql-tools to version 0.10.0 ([#31](https://github.com/tallyb/loopback-graphql/issues/31)) ([398386e](https://github.com/tallyb/loopback-graphql/commit/398386e)) 45 | * **package:** update graphql-tools to version 0.11.0 ([#46](https://github.com/tallyb/loopback-graphql/issues/46)) ([4991faa](https://github.com/tallyb/loopback-graphql/commit/4991faa)) 46 | * **tests:** Fix partial tests ([e5eb2cd](https://github.com/tallyb/loopback-graphql/commit/e5eb2cd)) 47 | 48 | 49 | ### Features 50 | 51 | * **app:** Upgrade to loopback 3.0 ([5dfe8fb](https://github.com/tallyb/loopback-graphql/commit/5dfe8fb)) 52 | 53 | 54 | 55 | 56 | # [0.10.0](https://github.com/tallyb/loopback-graphql/compare/v0.9.0...v0.10.0) (2017-01-06) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * **app:** Fix models names ([c21f454](https://github.com/tallyb/loopback-graphql/commit/c21f454)) 62 | * **app:** Fix wrong mixins ([93e8da7](https://github.com/tallyb/loopback-graphql/commit/93e8da7)) 63 | * **package:** update graphql-tools to version 0.9.0 ([#21](https://github.com/tallyb/loopback-graphql/issues/21)) ([1f0f309](https://github.com/tallyb/loopback-graphql/commit/1f0f309)) 64 | 65 | 66 | ### Features 67 | 68 | * **app:** Adding request to context ([0a3163d](https://github.com/tallyb/loopback-graphql/commit/0a3163d)) 69 | 70 | 71 | 72 | 73 | # [0.9.0](https://github.com/tallyb/loopback-graphql/compare/v0.8.0...v0.9.0) (2016-11-20) 74 | 75 | 76 | ### Features 77 | 78 | * **app:** Support complex models ([82e479c](https://github.com/tallyb/loopback-graphql/commit/82e479c)), closes [#7](https://github.com/tallyb/loopback-graphql/issues/7) 79 | 80 | 81 | 82 | 83 | # [0.8.0](https://github.com/tallyb/loopback-graphql/compare/v0.7.0...v0.8.0) (2016-11-15) 84 | 85 | 86 | ### Features 87 | 88 | * **app:** Resolvers with edges ([883e79d](https://github.com/tallyb/loopback-graphql/commit/883e79d)) 89 | * **app:** Support paginations ([5ca2257](https://github.com/tallyb/loopback-graphql/commit/5ca2257)) 90 | 91 | 92 | 93 | 94 | # [0.7.0](https://github.com/tallyb/loopback-graphql/compare/v0.6.0...v0.7.0) (2016-11-03) 95 | 96 | 97 | ### Bug Fixes 98 | 99 | * **typedefs:** Include generated properties ([a10c444](https://github.com/tallyb/loopback-graphql/commit/a10c444)) 100 | 101 | 102 | ### Features 103 | 104 | * **app:** Fix hidden attributes ([8d5c8a0](https://github.com/tallyb/loopback-graphql/commit/8d5c8a0)) 105 | 106 | 107 | 108 | 109 | # [0.6.0](https://github.com/tallyb/loopback-graphql/compare/v0.5.3...v0.6.0) (2016-10-18) 110 | 111 | 112 | ### Features 113 | 114 | * **App:** Support aggregated types ([b057b3b](https://github.com/tallyb/loopback-graphql/commit/b057b3b)), closes [#1](https://github.com/tallyb/loopback-graphql/issues/1) 115 | 116 | 117 | 118 | 119 | ## [0.5.3](https://github.com/tallyb/loopback-graphql/compare/v0.5.2...v0.5.3) (2016-10-16) 120 | 121 | 122 | ### Bug Fixes 123 | 124 | * **App:** Error on ReferencesMany relationship ([81f0821](https://github.com/tallyb/loopback-graphql/commit/81f0821)) 125 | 126 | 127 | 128 | 129 | ## [0.5.1](https://github.com/tallyb/loopback-graphql/compare/v0.5.0...0.5.1) (2016-10-15) 130 | 131 | 132 | ### Bug Fixes 133 | 134 | * **resolvers:** Fix resolving nested models ([c80b02d](https://github.com/tallyb/loopback-graphql/commit/c80b02d)) 135 | 136 | 137 | 138 | 139 | # [0.5.0](https://github.com/tallyb/loopback-graphql/compare/v0.3.0...v0.5.0) (2016-10-14) 140 | 141 | 142 | ### Features 143 | 144 | * **Query:** ID parameter to single object query ([83ab4c4](https://github.com/tallyb/loopback-graphql/commit/83ab4c4)) 145 | 146 | 147 | 148 | 149 | # [0.3.0](https://github.com/tallyb/loopback-graphql/compare/v0.2.1...v0.3.0) (2016-10-12) 150 | 151 | 152 | ### Bug Fixes 153 | 154 | * **app:** Fix relationships errors ([29716d1](https://github.com/tallyb/loopback-graphql/commit/29716d1)) 155 | 156 | 157 | ### Features 158 | 159 | * **app:** Add Delete Mutations ([e57dd20](https://github.com/tallyb/loopback-graphql/commit/e57dd20)) 160 | * **app:** Publish scripts ([caa66bf](https://github.com/tallyb/loopback-graphql/commit/caa66bf)) 161 | -------------------------------------------------------------------------------- /src/ast.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import { 3 | connectionTypeName, 4 | singularModelName, 5 | pluralModelName, 6 | methodName, 7 | edgeTypeName, 8 | sharedRelations, 9 | idToCursor, 10 | } from './utils'; 11 | import { findRelated, findAll, findOne, resolveConnection } from './execution'; 12 | import { ITypesHash } from './interfaces'; 13 | 14 | /*** Loopback Types - GraphQL types 15 | any - JSON 16 | Array - [JSON] 17 | Boolean = boolean 18 | Buffer - not supported 19 | Date - Date (custom scalar) 20 | GeoPoint - not supported 21 | null - not supported 22 | Number = float 23 | Object = JSON (custom scalar) 24 | String - string 25 | ***/ 26 | 27 | let types: ITypesHash = {}; 28 | 29 | const exchangeTypes = { 30 | 'any': 'JSON', 31 | 'Any': 'JSON', 32 | 'Number': 'Int', 33 | 'number': 'Int', 34 | 'Object': 'JSON', 35 | 'object': 'JSON', 36 | }; 37 | 38 | const SCALARS = { 39 | any: 'JSON', 40 | number: 'Float', 41 | string: 'String', 42 | boolean: 'Boolean', 43 | objectid: 'ID', 44 | date: 'Date', 45 | object: 'JSON', 46 | now: 'Date', 47 | guid: 'ID', 48 | uuid: 'ID', 49 | uuidv4: 'ID', 50 | geopoint: 'GeoPoint', 51 | }; 52 | 53 | const PAGINATION = 'where: JSON, after: String, first: Int, before: String, last: Int'; 54 | const IDPARAMS = 'id: ID!'; 55 | 56 | function getScalar(type: string) { 57 | return SCALARS[type.toLowerCase().trim()]; 58 | } 59 | 60 | function toTypes(union: string[]) { 61 | return _.map(union, type => { 62 | return getScalar(type) ? getScalar(type) : type; 63 | }); 64 | } 65 | 66 | function mapProperty(model: any, property: any, modelName: string, propertyName: string) { 67 | if (property.deprecated) { 68 | return; 69 | } 70 | types[modelName].fields[propertyName] = { 71 | required: property.required, 72 | hidden: model.definition.settings.hidden && model.definition.settings.hidden.indexOf(propertyName) !== -1, 73 | }; 74 | let currentProperty = types[modelName].fields[propertyName]; 75 | 76 | let typeName = `${modelName}_${propertyName}`; 77 | let propertyType = property.type; 78 | 79 | if (propertyType.name === 'Array') { // JSON Array 80 | currentProperty.list = true; 81 | currentProperty.gqlType = 'JSON'; 82 | currentProperty.scalar = true; 83 | return; 84 | } 85 | 86 | if (_.isArray(property.type)) { 87 | currentProperty.list = true; 88 | propertyType = property.type[0]; 89 | } 90 | 91 | let scalar = getScalar(propertyType.name); 92 | if (property.defaultFn) { 93 | scalar = getScalar(property.defaultFn); 94 | } 95 | if (scalar) { 96 | currentProperty.scalar = true; 97 | currentProperty.gqlType = scalar; 98 | if (property.enum) { // enum has a dedicated type but no input type is required 99 | types[typeName] = { 100 | values: property.enum, 101 | category: 'ENUM', 102 | }; 103 | currentProperty.gqlType = typeName; 104 | } 105 | } 106 | 107 | if (propertyType.name === 'ModelConstructor' && property.defaultFn !== 'now') { 108 | currentProperty.gqlType = propertyType.modelName; 109 | let union = propertyType.modelName.split('|'); 110 | //type is a union 111 | if (union.length > 1) { // union type 112 | types[typeName] = { // creating a new union type 113 | category: 'UNION', 114 | values: toTypes(union), 115 | }; 116 | } else if (propertyType.settings && propertyType.settings.anonymous && propertyType.definition) { 117 | currentProperty.gqlType = typeName; 118 | types[typeName] = { 119 | category: 'TYPE', 120 | input: true, 121 | fields: {}, 122 | }; // creating a new type 123 | _.forEach(propertyType.definition.properties, (p, key) => { 124 | mapProperty(propertyType, p, typeName, key); 125 | }); 126 | } 127 | } 128 | } 129 | 130 | function mapRelation(rel: any, modelName: string, relName: string) { 131 | types[modelName].fields[relName] = { 132 | relation: true, 133 | embed: rel.embed, 134 | gqlType: connectionTypeName(rel.modelTo), 135 | args: PAGINATION, 136 | resolver: (obj, args) => { 137 | return findRelated(rel, obj, args); 138 | }, 139 | }; 140 | } 141 | 142 | /* 143 | function generateReturns(name, props) { 144 | if (_.isObject(props)) { 145 | props = [props]; 146 | } 147 | let args; 148 | args = _.map(props, prop => { 149 | if (_.isArray(prop.type)) { 150 | return `${prop.arg}: [${toType(prop.type[0])}]${prop.required ? '!' : ''}`; 151 | } else if (toType(prop.type)) { 152 | return `${prop.arg}: ${toType(prop.type)}${prop.required ? '!' : ''}`; 153 | } 154 | return ''; 155 | }).join(' \n '); 156 | return args ? `{${args}}` : ''; 157 | } 158 | 159 | function generateAccepts(name, props) { 160 | let ret = _.map(props, prop => { 161 | let propType = prop.type; 162 | if (_.isArray(prop.type)) { 163 | propType = prop.type[0]; 164 | } 165 | return propType ? `${prop.arg}: [${toType(prop.type[0])}]${prop.required ? '!' : ''}` : ''; 166 | }).join(' \n '); 167 | return ret ? `(${ret})` : ''; 168 | 169 | } 170 | */ 171 | 172 | function addRemoteHooks(model: any) { 173 | 174 | _.map(model.sharedClass._methods, (method: any) => { 175 | if (method.accessType !== 'READ' && method.http.path) { 176 | let acceptingParams = '', 177 | returnType = 'JSON'; 178 | method.accepts.map(function (param) { 179 | let paramType = ''; 180 | if (typeof param.type === 'object') { 181 | paramType = 'JSON'; 182 | } else { 183 | if (!SCALARS[param.type.toLowerCase()]) { 184 | paramType = `${param.type}Input`; 185 | } else { 186 | paramType = _.upperFirst(param.type); 187 | } 188 | } 189 | if (param.arg) { 190 | acceptingParams += `${param.arg}: ${exchangeTypes[paramType] || paramType} `; 191 | } 192 | }); 193 | if (method.returns && method.returns[0]) { 194 | if (!SCALARS[method.returns[0].type] && typeof method.returns[0].type !== 'object') { 195 | returnType = `${method.returns[0].type}`; 196 | } else { 197 | returnType = `${_.upperFirst(method.returns[0].type)}`; 198 | if (typeof method.returns[0].type === 'object') { 199 | returnType = 'JSON'; 200 | } 201 | } 202 | } 203 | types.Mutation.fields[`${methodName(method, model)}`] = { 204 | relation: true, 205 | args: acceptingParams, 206 | gqlType: `${exchangeTypes[returnType] || returnType}`, 207 | }; 208 | } 209 | }); 210 | } 211 | 212 | function mapRoot(model) { 213 | types.Query.fields[singularModelName(model)] = { 214 | relation: true, 215 | args: IDPARAMS, 216 | root: true, 217 | gqlType: singularModelName(model), 218 | resolver: (obj, args /*, context*/) => { 219 | findOne(model, obj, args); 220 | }, 221 | }; 222 | 223 | types.Query.fields[pluralModelName(model)] = { 224 | relation: true, 225 | root: true, 226 | args: PAGINATION, 227 | gqlType: connectionTypeName(model), 228 | resolver: (obj, args) => { 229 | findAll(model, obj, args); 230 | }, 231 | }; 232 | 233 | types.Mutation.fields[`save${singularModelName(model)}`] = { 234 | relation: true, 235 | args: `obj: ${singularModelName(model)}Input!`, 236 | gqlType: singularModelName(model), 237 | resolver: (context, args) => model.upsert(args.obj, context), 238 | }; 239 | 240 | types.Mutation.fields[`delete${singularModelName(model)}`] = { 241 | relation: true, 242 | args: IDPARAMS, 243 | gqlType: ` ${singularModelName(model)}`, 244 | resolver: (context, args) => { 245 | return model.findById(args.id, context) 246 | .then(instance => instance.destroy()); 247 | }, 248 | }; 249 | // _.each(model.sharedClass.methods, method => { 250 | // if (method.accessType !== 'READ' && method.http.path) { 251 | // let methodName = methodName(method, model); 252 | // types.Mutation.fields[methodName] = { 253 | // gqlType: `${generateReturns(method.name, method.returns)}`, 254 | // args: `${generateAccepts(method.name, method.accepts)}` 255 | // } 256 | 257 | // return `${methodName(method)} 258 | // ${generateAccepts(method.name, method.accepts)} 259 | 260 | // : JSON`; 261 | // } else { 262 | // return undefined; 263 | // } 264 | // }); 265 | addRemoteHooks(model); 266 | } 267 | 268 | function mapConnection(model) { 269 | types[connectionTypeName(model)] = { 270 | connection: true, 271 | category: 'TYPE', 272 | fields: { 273 | pageInfo: { 274 | required: true, 275 | gqlType: 'pageInfo', 276 | }, 277 | edges: { 278 | list: true, 279 | gqlType: edgeTypeName(model), 280 | resolver: (obj /*, args, context*/) => { 281 | return _.map(obj.list, node => { 282 | return { 283 | cursor: idToCursor(node[model.getIdName()]), 284 | node: node, 285 | }; 286 | }); 287 | }, 288 | }, 289 | totalCount: { 290 | gqlType: 'Int', 291 | scalar: true, 292 | resolver: (obj /*, args, context*/) => { 293 | return obj.count; 294 | }, 295 | }, 296 | [model.pluralModelName]: { 297 | gqlType: singularModelName(model), 298 | list: true, 299 | resolver: (obj /*, args, context*/) => { 300 | return obj.list; 301 | }, 302 | }, 303 | }, 304 | resolver: (/*obj, args, context*/) => { 305 | return resolveConnection(model); 306 | }, 307 | }; 308 | types[edgeTypeName(model)] = { 309 | category: 'TYPE', 310 | fields: { 311 | node: { 312 | gqlType: singularModelName(model), 313 | required: true, 314 | }, 315 | cursor: { 316 | gqlType: 'String', 317 | required: true, 318 | }, 319 | }, 320 | }; 321 | 322 | } 323 | export function abstractTypes(models: any[]): ITypesHash { 324 | //building all models types & relationships 325 | types.pageInfo = { 326 | category: 'TYPE', 327 | fields: { 328 | hasNextPage: { 329 | gqlType: 'Boolean', 330 | required: true, 331 | }, 332 | hasPreviousPage: { 333 | gqlType: 'Boolean', 334 | required: true, 335 | }, 336 | startCursor: { 337 | gqlType: 'String', 338 | }, 339 | endCursor: { 340 | gqlType: 'String', 341 | }, 342 | }, 343 | }; 344 | types.Query = { 345 | category: 'TYPE', 346 | fields: {}, 347 | }; 348 | types.Mutation = { 349 | category: 'TYPE', 350 | fields: {}, 351 | }; 352 | 353 | _.forEach(models, model => { 354 | if (model.shared) { 355 | mapRoot(model); 356 | } 357 | types[singularModelName(model)] = { 358 | category: 'TYPE', 359 | input: true, 360 | fields: {}, 361 | }; 362 | _.forEach(model.definition.properties, (property, key) => { 363 | mapProperty(model, property, singularModelName(model), key); 364 | }); 365 | 366 | mapConnection(model); 367 | _.forEach(sharedRelations(model), rel => { 368 | mapRelation(rel, singularModelName(model), rel.name); 369 | mapConnection(rel.modelTo); 370 | }); 371 | }); 372 | return types; 373 | } 374 | --------------------------------------------------------------------------------