diff --git a/README.md b/README.md index 153c023..b4826ae 100644 --- a/README.md +++ b/README.md @@ -14,116 +14,13 @@ $ yarn add salesforce-graphql jsforce ``` -### Example +## Examples -#### `app.ts` +Refer to the `examples` folder for examples: -```ts -import * as jsforce from 'jsforce'; -import * as path from 'path'; -import * as fs from 'fs'; +> https://github.com/jpmonette/salesforce-graphql/tree/master/examples/apollo. -import { GraphQLServer } from 'graphql-yoga'; -import { Binding } from 'salesforce-graphql'; - -const schemaFile = path.join(__dirname, 'schema.graphql'); -const typeDefs = fs.readFileSync(schemaFile, 'utf8'); - -const { USERNAME, PASSWORD } = process.env; - -const resolvers = { - Query: { - Accounts: (parent, args, context, info) => - context.db.query({}, info).then(res => res.records), - Account: (parent, args, context, info) => - context.db.query({}, info).then(res => res.records[0]), - Contacts: (parent, args, context, info) => - context.db.query({}, info).then(res => res.records), - Contact: (parentobj, args, context, info) => - context.db.query({}, info).then(res => res.records[0]), - }, - Account: { - Contacts: (parent, args, context, info) => - context.db.query({ AccountId: parent.Id }, info).then(res => res.records), - }, - Contact: { - Account: (parent, args, context, info) => - context.db.query({ Id: parent.AccountId }, info).then(res => res.records[0]), - }, -}; - -const conn = new jsforce.Connection({}); - -function init() { - const db = new Binding({ conn }); - - const server = new GraphQLServer({ - typeDefs, - resolvers, - context: req => ({ ...req, db }), - }); - - server.start({ playground: '/playground' }, ({ port }) => - console.log('Server is running on localhost:' + port) - ); -} - -conn.login(USERNAME, PASSWORD, (err, userinfo) => init()); -``` - -#### `schema.graphql` - -```graphql -type Query { - Account(Id: ID!): Account - Accounts(limit: Int): [Account] - Contact(Id: ID!): Contact - Contacts(limit: Int): [Contact] -} - -type Account { - Id: ID! - IsDeleted: Boolean - Name: String - Type: String - - Contacts(limit: Int): [Contact] -} - -type Contact { - Id: ID! - Account: Account - AccountId: String - LastName: String - FirstName: String - Salutation: String - Name: String -} -``` - -When you are ready, start the GraphQL server: - -```sh -$ yarn start -``` - -Head over to `http://localhost:4000/playground` to test with the following query: - -```graphql -{ - Account(Id: "001E000001KnMkTIAV") { - Id - Name - Contacts(limit: 1) { - Name - AccountId - Account { - Name - } - } - } -} -``` +## Sample GraphIQL Output ![Sample Output](assets/output.png) @@ -135,10 +32,10 @@ Head over to `http://localhost:4000/playground` to test with the following query ## References -- [`salesforce-graphql` on NPM](https://www.npmjs.com/package/salesforce-graphql) -- Learn more about [GraphQL](http://graphql.org/) -- [Salesforce REST API](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_what_is_rest_api.htm) documentation +* [`salesforce-graphql` on NPM](https://www.npmjs.com/package/salesforce-graphql) +* Learn more about [GraphQL](http://graphql.org/) +* [Salesforce REST API](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_what_is_rest_api.htm) documentation ## Extra -- Looking for [new opportunities](https://mavens.com/careers/)? Have a look at [Mavens](https://mavens.com/) website! \ No newline at end of file +* Looking for [new opportunities](https://mavens.com/careers/)? Have a look at [Mavens](https://mavens.com/) website! diff --git a/examples/apollo/package.json b/examples/apollo/package.json new file mode 100644 index 0000000..12c1d54 --- /dev/null +++ b/examples/apollo/package.json @@ -0,0 +1,27 @@ +{ + "name": "apollo", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "scripts": { + "start": "nodemon -e ts -x ts-node src/index.ts" + }, + "dependencies": { + "apollo-server-express": "^1.3.5", + "body-parser": "^1.18.2", + "cookie-parser": "^1.4.3", + "cookie-session": "^2.0.0-beta.3", + "dataloader": "^1.4.0", + "dotenv": "^5.0.1", + "express": "^4.16.3", + "express-session": "^1.15.6", + "graphql": "^0.13.2", + "graphql-tools": "^2.24.0", + "jsforce": "^1.8.4", + "passport": "^0.4.0", + "passport-forcedotcom": "^0.1.4" + }, + "devDependencies": { + "@types/node": "^9.6.6" + } +} diff --git a/examples/apollo/src/index.ts b/examples/apollo/src/index.ts new file mode 100644 index 0000000..0c9bdac --- /dev/null +++ b/examples/apollo/src/index.ts @@ -0,0 +1,68 @@ +import { graphiqlExpress, graphqlExpress } from 'apollo-server-express'; +import * as bodyParser from 'body-parser'; +import * as cookieParser from 'cookie-parser'; +import * as cookieSession from 'cookie-session'; +import * as DataLoader from 'dataloader'; +import * as express from 'express'; +import { makeExecutableSchema } from 'graphql-tools'; +import * as jsforce from 'jsforce'; +import * as passport from 'passport'; +import { Strategy } from 'passport-forcedotcom'; + +import { cookieOptions, strategyOptions } from './lib/config'; +import typeDefs from './lib/typeDefs'; +import resolvers from './resolvers'; + +const app = express(); + +passport.use(new Strategy(strategyOptions, (token, _1, _2, done) => done(null, token.params))); +app.use(cookieParser()); +app.use(cookieSession(cookieOptions)); +app.use(passport.initialize()); +app.use(passport.session()); + +passport.serializeUser((token, done) => done(null, token)); +passport.deserializeUser((token, done) => done(null, token)); + +app.get('/auth/salesforce', passport.authenticate('forcedotcom')); + +app.get( + '/services/oauth2/callback', + passport.authenticate('forcedotcom', { failureRedirect: '/error' }), + (req, res) => res.redirect('/graphiql') +); + +app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' })); + +const executeQuery = ({ access_token, instance_url }, query) => + new jsforce.Connection({ + version: '42.0', + accessToken: access_token, + instanceUrl: instance_url, + }) + .query(query) + .then(response => response.records); + +const insert = ({ access_token, instance_url }, sobjectName, data) => + new jsforce.Connection({ + version: '42.0', + accessToken: access_token, + instanceUrl: instance_url, + }) + .sobject(sobjectName) + .create(data); + +app.use('/graphql', bodyParser.json(), (req: any, res, next) => { + const queryLoader = new DataLoader(keys => + Promise.all(keys.map(query => executeQuery(req.user, query))) + ); + + const db = { insert: (sobjectName, data) => insert(req.user, sobjectName, data) }; + + return graphqlExpress({ + schema: makeExecutableSchema({ typeDefs: typeDefs, resolvers: resolvers as any }), + context: { user: req.user, loaders: { query: queryLoader }, db }, + })(req, res, next); +}); + +app.listen(3000, () => console.log('Now browse to localhost:3000/graphiql')); diff --git a/examples/apollo/src/lib/config.ts b/examples/apollo/src/lib/config.ts new file mode 100644 index 0000000..9853376 --- /dev/null +++ b/examples/apollo/src/lib/config.ts @@ -0,0 +1,23 @@ +require('dotenv').config(); + +export const { + SALESFORCE_USERNAME, + SALESFORCE_PASSWORD, + JWT_SECRET, + CALLBACK_URL, + CLIENT_ID, + CLIENT_SECRET, +} = process.env; + +export const cookieOptions = { + name: 'session', + keys: ['key1'], + maxAge: 24 * 60 * 60 * 1000, +}; + +export const strategyOptions = { + clientID: CLIENT_ID, + clientSecret: CLIENT_SECRET, + scope: ['id', 'chatter_api', 'api'], + callbackURL: CALLBACK_URL, +}; diff --git a/examples/apollo/src/lib/fieldLists.ts b/examples/apollo/src/lib/fieldLists.ts new file mode 100644 index 0000000..984338a --- /dev/null +++ b/examples/apollo/src/lib/fieldLists.ts @@ -0,0 +1,89 @@ +export const contactFields = [ + 'AccountId', + 'AssistantName', + 'AssistantPhone', + 'Birthdate', + 'CreatedById', + 'CreatedDate', + 'CurrencyIsoCode', + 'Department', + 'Description', + 'Email', + 'EmailBouncedDate', + 'EmailBouncedReason', + 'Fax', + 'FirstName', + 'HomePhone', + 'Id', + 'IsDeleted', + 'IsEmailBounced', + 'Jigsaw', + 'JigsawContactId', + 'LastActivityDate', + 'LastCURequestDate', + 'LastCUUpdateDate', + 'LastModifiedById', + 'LastModifiedDate', + 'LastName', + 'LastReferencedDate', + 'LastViewedDate', + 'LeadSource', + 'MailingAddress', + 'MailingGeocodeAccuracy', + 'MasterRecordId', + 'MobilePhone', + 'Name', + 'OtherAddress', + 'OtherGeocodeAccuracy', + 'OtherPhone', + 'OwnerId', + 'Phone', + 'PhotoUrl', + 'ReportsToId', + 'Salutation', + 'SystemModstamp', + 'Title', +]; +export const accountFields = [ + 'AccountNumber', + 'AccountSource', + 'AnnualRevenue', + 'BillingAddress', + 'BillingGeocodeAccuracy', + 'CreatedById', + 'CreatedDate', + 'CurrencyIsoCode', + 'Description', + 'Fax', + 'Id', + 'Industry', + 'IsCustomerPortal', + 'IsDeleted', + 'IsPartner', + 'Jigsaw', + 'JigsawCompanyId', + 'LastActivityDate', + 'LastModifiedById', + 'LastModifiedDate', + 'LastReferencedDate', + 'LastViewedDate', + 'MasterRecordId', + 'Name', + 'NumberOfEmployees', + 'OwnerId', + 'Ownership', + 'ParentId', + 'Phone', + 'PhotoUrl', + 'Rating', + 'RecordTypeId', + 'ShippingAddress', + 'ShippingGeocodeAccuracy', + 'Sic', + 'SicDesc', + 'Site', + 'SystemModstamp', + 'TickerSymbol', + 'Type', + 'Website', +]; diff --git a/examples/apollo/src/lib/schema.graphql b/examples/apollo/src/lib/schema.graphql new file mode 100644 index 0000000..3e486e2 --- /dev/null +++ b/examples/apollo/src/lib/schema.graphql @@ -0,0 +1,153 @@ +type SaveResult { + id: String +} + +type Mutation { + createAccount(Name: String): SaveResult +} + +type Query { + Account(Id: ID!): Account + Accounts(limit: Int!, sortField: String, sortOrder: String): [Account] + Contact(Id: ID!): Contact + Contacts(limit: Int, sortField: String, sortOrder: String): [Contact] +} + +type Account { + Id: ID! @unique + IsDeleted: Boolean + MasterRecordId: String + Name: String + Type: String + RecordTypeId: String + ParentId: String + BillingStreet: String + BillingCity: String + BillingState: String + BillingPostalCode: String + BillingCountry: String + BillingLatitude: Int + BillingLongitude: Int + BillingGeocodeAccuracy: String + BillingAddress: String + ShippingStreet: String + ShippingCity: String + ShippingState: String + ShippingPostalCode: String + ShippingCountry: String + ShippingLatitude: Int + ShippingLongitude: Int + ShippingGeocodeAccuracy: String + ShippingAddress: String + Phone: String + Fax: String + AccountNumber: String + Website: String + PhotoUrl: String + Sic: String + Industry: String + AnnualRevenue: Int + NumberOfEmployees: Int + Ownership: String + TickerSymbol: String + Description: String + Rating: String + Site: String + CurrencyIsoCode: String + OwnerId: String + CreatedDate: String + CreatedById: String + LastModifiedDate: String + LastModifiedById: String + SystemModstamp: String + LastActivityDate: String + LastViewedDate: String + LastReferencedDate: String + IsPartner: Boolean + IsCustomerPortal: Boolean + Jigsaw: String + JigsawCompanyId: String + AccountSource: String + SicDesc: String + jpmonette__CustomerPriority__c: String + jpmonette__SLA__c: String + jpmonette__Active__c: String + jpmonette__UpsellOpportunity__c: String + jpmonette__SLASerialNumber__c: String + jpmonette__SLAExpirationDate__c: String + jpmonette__Test_Formula__c: String + jpmonette__Contact__c: String + jpmonette__DATETIME_TEST__c: String + jpmonette__EXTERNAL__c: String + jpmonette__Parent_Account__c: String + jpmonette__Total_Opp__c: Int + + Contacts(limit: Int): [Contact] +} + +type Contact { + Id: ID! + IsDeleted: Boolean + MasterRecord: Contact + MasterRecordId: String + Account: Account + AccountId: String + LastName: String + FirstName: String + Salutation: String + Name: String + OtherStreet: String + OtherCity: String + OtherState: String + OtherPostalCode: String + OtherCountry: String + OtherLatitude: Int + OtherLongitude: Int + OtherGeocodeAccuracy: String + OtherAddress: String + MailingStreet: String + MailingCity: String + MailingState: String + MailingPostalCode: String + MailingCountry: String + MailingLatitude: Int + MailingLongitude: Int + MailingGeocodeAccuracy: String + MailingAddress: String + Phone: String + Fax: String + MobilePhone: String + HomePhone: String + OtherPhone: String + AssistantPhone: String + ReportsTo: Contact + ReportsToId: String + Email: String + Title: String + Department: String + AssistantName: String + LeadSource: String + Birthdate: String + Description: String + CurrencyIsoCode: String + # Owner: User + OwnerId: String + CreatedDate: String + # CreatedBy: User + CreatedById: String + LastModifiedDate: String + # LastModifiedBy: User + LastModifiedById: String + SystemModstamp: String + LastActivityDate: String + LastCURequestDate: String + LastCUUpdateDate: String + LastViewedDate: String + LastReferencedDate: String + EmailBouncedReason: String + EmailBouncedDate: String + IsEmailBounced: Boolean + PhotoUrl: String + Jigsaw: String + JigsawContactId: String +} diff --git a/examples/apollo/src/lib/typeDefs.ts b/examples/apollo/src/lib/typeDefs.ts new file mode 100644 index 0000000..735f695 --- /dev/null +++ b/examples/apollo/src/lib/typeDefs.ts @@ -0,0 +1,5 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +const schemaFile = path.join(__dirname, 'schema.graphql'); +export default fs.readFileSync(schemaFile, 'utf8') as any; diff --git a/examples/apollo/src/resolvers.ts b/examples/apollo/src/resolvers.ts new file mode 100644 index 0000000..2616891 --- /dev/null +++ b/examples/apollo/src/resolvers.ts @@ -0,0 +1,73 @@ +import { SOQL } from 'salesforce-queries'; + +import { accountFields, contactFields } from './lib/fieldLists'; + +const resolvers = { + Mutation: { + createAccount: (input, data, { db }) => db.insert('Account', data), + }, + Query: { + Accounts: (parent, { limit }, { loaders }, info) => { + const query = new SOQL('Account') + .select(accountFields) + .limit(limit) + .build(); + + return loaders.query.load(query); + }, + Account: (parent, { Id }, { loaders }, info) => { + const query = new SOQL('Account') + .select(accountFields) + .where('Id', '=', Id) + .build(); + + return loaders.query.load(query).then(records => records[0]); + }, + Contacts: (parent, { limit }, { loaders, user }, info) => { + const query = new SOQL('Contact') + .select(contactFields) + .limit(limit) + .build(); + + return loaders.query.load(query); + }, + Contact: (parent, { Id }, { loaders }, info) => { + const query = new SOQL('Contact') + .select(contactFields) + .where('Id', '=', Id) + .build(); + + return loaders.query.load(query).then(records => records[0]); + }, + }, + Account: { + Contacts: (parent, args, { loaders }, info) => { + if (!parent.Id) { + return null; + } + + const query = new SOQL('Contact') + .select(contactFields) + .where('AccountId', '=', parent.Id) + .build(); + + return loaders.query.load(query); + }, + }, + Contact: { + Account: (parent, args, { loaders }, info) => { + if (!parent.AccountId) { + return null; + } + + const query = new SOQL('Account') + .select(accountFields) + .where('Id', '=', parent.AccountId) + .build(); + + return loaders.query.load(query).then(records => records[0]); + }, + }, +}; + +export default resolvers; diff --git a/examples/apollo/tsconfig.json b/examples/apollo/tsconfig.json new file mode 100644 index 0000000..dc66c90 --- /dev/null +++ b/examples/apollo/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "commonjs", + "outDir": "build", + "target": "es2016", + "allowJs": true, + "lib": ["esnext", "dom"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/node_modules/*"] +} diff --git a/tsconfig.json b/tsconfig.json index 05a306c..eb3b0d0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,8 +14,8 @@ "strictFunctionTypes": true, "strictPropertyInitialization": true, "strictNullChecks": true, - "lib": ["esnext","dom"] + "lib": ["esnext", "dom"] }, "include": ["src/**/*"], - "exclude": ["node_modules", "**/node_modules/*"] + "exclude": ["node_modules", "**/node_modules/*", "examples", "**/examples/*"] }