diff --git a/documentation/getting-started.md b/documentation/getting-started.md index 77f8ee3..75e0fe7 100644 --- a/documentation/getting-started.md +++ b/documentation/getting-started.md @@ -40,6 +40,20 @@ When starting a new project or adding glide to an existing project, you may exec glide init https://my-salesforce-instance.salesforce.com ``` +### Sandbox Setup + +If you wish to use glide with a sandbox environment, you may generate an additional JSON configuration file by running the `init` command again with the `--sandbox` option. When `--sandbox` is passed to the `init` command, a boolean `"sandbox": true` property is added to the resulting JSON configuration file. This option instructs the authentication layer of glide to use the correct login url (https://test.salesforce.com). + +```sh +glide init https://my-salesforce-sandbox.salesforce.com --sandbox +``` + +In order to prevent name collisions with the default JSON configuration file generated from the `init` command, `glide.sandbox.json` will be used as the file name if the optional `path` argument is not provided. If you wish to use another file name or path, you may do so by providing the `path` argument. + +```sh +glide init https://my-salesforce-sandbox.salesforce.com sandbox.json --sandbox +``` + **Note:** If you have not used Glide with your Salesforce instance before, you may be prompted to login. More information about how authentication works in Glide can be found in the [_Advanced Topics_](./advanced-topics.md) section of the guides. @@ -48,6 +62,16 @@ If you have not used Glide with your Salesforce instance before, you may be prom Once you have a JSON configuration file in the root directory of your project, you may spawn a GraphQL server by executing the `serve` command. When executing the `serve` command in a development environment, your default browser will open a [GraphQL Playground](https://github.com/prisma/graphql-playground) playground page to start exploring your data. +```sh +glide serve +``` + +To use a different JSON configuration file then the default `glide.json` generated by the `init` command, you may do so by providing a the path to the configuration file as a positional argument to the `serve` command. + +```sh +glide serve glide.sandbox.json +``` + You manually edit your JSON configuration file if you wish to modify the mutations, queries, and/or data types exposed by your GraphQL server. Information about how to configure the generated GraphQL schema can be found in the [_Advanced Topics_](./advanced-topics.md) section of the guides. GraphQL Playground diff --git a/package.json b/package.json index ebdc92a..72482d8 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@glide/runtime": "^0.0.1", "@zakgolba/jsonptr": "^1.1.0", "body-parser": "^1.19.0", + "boxen": "^4.1.0", "chalk": "^2.4.2", "commander": "^3.0.1", "cross-fetch": "^3.0.4", diff --git a/packages/glide-authenticate/package.json b/packages/glide-authenticate/package.json index 1f8e450..e8a902b 100644 --- a/packages/glide-authenticate/package.json +++ b/packages/glide-authenticate/package.json @@ -5,17 +5,20 @@ "main": "dist/lib.js", "types": "dist/lib.d.ts", "scripts": { + "deploy": "serverless deploy", "lint": "tslint --project tsconfig.json" }, "dependencies": { "aws-sdk": "^2.534.0", "jsforce": "^1.9.2", + "lodash": "^4.17.15", "nconf": "^0.10.0" }, "devDependencies": { "@types/aws-lambda": "8.10.32", "@types/aws-sdk": "2.7.0", "@types/jsforce": "1.9.9", + "@types/lodash": "4.14.139", "@types/nconf": "0.10.0", "@types/node": "12.7.5", "serverless": "^1.52.2" diff --git a/packages/glide-authenticate/serverless.yml b/packages/glide-authenticate/serverless.yml index 8d633ea..234bde4 100644 --- a/packages/glide-authenticate/serverless.yml +++ b/packages/glide-authenticate/serverless.yml @@ -1,4 +1,4 @@ -service: glide-authenticate +service: glide-oauth provider: name: aws runtime: nodejs8.10 @@ -7,23 +7,23 @@ package: - dist/** functions: callback: - handler: dist/lib.callback + handler: dist/events/oauth/callback.default events: - http: path: oauth/callback method: get connect: - handler: dist/lib.connect + handler: dist/events/connect.default events: - websocket: route: $connect message: - handler: dist/lib.message + handler: dist/events/message.default events: - websocket: route: $default refresh: - handler: dist/lib.refresh + handler: dist/events/oauth/refresh.default events: - http: path: oauth/refresh diff --git a/packages/glide-authenticate/src/events/connect.ts b/packages/glide-authenticate/src/events/connect.ts new file mode 100644 index 0000000..4b1b433 --- /dev/null +++ b/packages/glide-authenticate/src/events/connect.ts @@ -0,0 +1,5 @@ +import { handler } from "../utilities"; + +export default handler(async () => { + // noop +}); diff --git a/packages/glide-authenticate/src/events/message.ts b/packages/glide-authenticate/src/events/message.ts new file mode 100644 index 0000000..49b0f4c --- /dev/null +++ b/packages/glide-authenticate/src/events/message.ts @@ -0,0 +1,17 @@ +import { get } from "lodash"; + +import { MessageType } from "../types"; +import { handler, oauth2, send } from "../utilities"; + +export default handler(async request => { + const { connectionId, domainName, stage } = request.requestContext; + const environment = get(JSON.parse(request.body || "{}"), "data.environment"); + const result = oauth2.configure(environment).getAuthorizationUrl({ + state: JSON.stringify({ connectionId, domainName, stage }), + }); + + await send(request.requestContext, { + data: result, + type: MessageType.Initialize, + }); +}); diff --git a/packages/glide-authenticate/src/events/oauth/callback.ts b/packages/glide-authenticate/src/events/oauth/callback.ts new file mode 100644 index 0000000..c16f441 --- /dev/null +++ b/packages/glide-authenticate/src/events/oauth/callback.ts @@ -0,0 +1,26 @@ +import { Connection } from "jsforce"; + +import { MessageType } from "../../types"; +import { handler, params, oauth2, send } from "../../utilities"; + +export default handler(async request => { + const environment = params.get(request, "environment"); + const connection = new Connection({ + oauth2: oauth2.configure(environment), + }); + + await connection.authorize(params.require(request, "code")); + await send(JSON.parse(params.require(request, "state")), { + data: { + accessToken: connection.accessToken, + oauth2: oauth2.options(environment), + // @ts-ignore + refreshToken: connection.refreshToken, + }, + type: MessageType.Authenticated, + }); + + return { + message: "You have been successfully logged in", + }; +}); diff --git a/packages/glide-authenticate/src/events/oauth/refresh.ts b/packages/glide-authenticate/src/events/oauth/refresh.ts new file mode 100644 index 0000000..910e835 --- /dev/null +++ b/packages/glide-authenticate/src/events/oauth/refresh.ts @@ -0,0 +1,12 @@ +import { handler, oauth2 } from "../../utilities"; + +export default handler(async request => { + const { environment, token } = JSON.parse(request.body || "{}"); + const result = await oauth2.configure(environment).refreshToken(token); + + return { + oauth2: oauth2.options(environment), + accessToken: result.access_token, + refreshToken: result.refresh_token, + }; +}); diff --git a/packages/glide-authenticate/src/lib.ts b/packages/glide-authenticate/src/lib.ts index cb1bbdf..e69de29 100644 --- a/packages/glide-authenticate/src/lib.ts +++ b/packages/glide-authenticate/src/lib.ts @@ -1,60 +0,0 @@ -import { Connection, OAuth2 } from "jsforce"; -import nconf from "nconf"; - -import { handler, params, send } from "./utilities"; - -const oauth2 = new OAuth2(nconf.env("__").get("oauth")); - -const enum MessageType { - Authenticated = "AUTHENTICATED", - Initialize = "INITIALIZE", -} - -export const callback = handler(async request => { - const client = JSON.parse(params.require(request, "state")); - const connection = new Connection({ oauth2 }); - - await connection.authorize(params.require(request, "code")); - - await send(client, { - data: { - accessToken: connection.accessToken, - oauth2: nconf.get("oauth"), - // @ts-ignore - refreshToken: connection.refreshToken, - }, - type: MessageType.Authenticated, - }); - - return { - message: "You have been successfully logged in", - }; -}); - -export const connect = handler(async () => { - // noop -}); - -export const message = handler(async request => { - const state = JSON.stringify({ - connectionId: request.requestContext.connectionId, - domainName: request.requestContext.domainName, - stage: request.requestContext.stage, - }); - - await send(request.requestContext, { - data: oauth2.getAuthorizationUrl({ state }), - type: MessageType.Initialize, - }); -}); - -export const refresh = handler(async request => { - const { token } = JSON.parse(request.body || "{}"); - const results = await oauth2.refreshToken(token); - - return { - oauth2: nconf.get("oauth"), - accessToken: results.access_token, - refreshToken: results.refresh_token, - }; -}); diff --git a/packages/glide-authenticate/src/types.ts b/packages/glide-authenticate/src/types.ts new file mode 100644 index 0000000..d48d87c --- /dev/null +++ b/packages/glide-authenticate/src/types.ts @@ -0,0 +1,22 @@ +export enum Environment { + Default = "default", + Sandbox = "sandbox", +} + +export namespace Environment { + export function from(input?: string): Environment { + switch (input) { + case Environment.Sandbox: { + return Environment.Sandbox; + } + default: { + return Environment.Default; + } + } + } +} + +export const enum MessageType { + Authenticated = "AUTHENTICATED", + Initialize = "INITIALIZE", +} diff --git a/packages/glide-authenticate/src/utilities.ts b/packages/glide-authenticate/src/utilities.ts index db67f3f..ca2c4ee 100644 --- a/packages/glide-authenticate/src/utilities.ts +++ b/packages/glide-authenticate/src/utilities.ts @@ -1,5 +1,9 @@ import lambda, { Context } from "aws-lambda"; import { ApiGatewayManagementApi } from "aws-sdk"; +import { OAuth2, OAuth2Options } from "jsforce"; +import nconf from "nconf"; + +import { Environment } from "./types"; let apiGateway: ApiGatewayManagementApi | null = null; @@ -11,6 +15,8 @@ export type Response = lambda.APIGatewayProxyResult; export type Responder = (request: Request, context: Context) => Promise; export function handler(responder: Responder): lambda.APIGatewayProxyHandler { + nconf.env("__"); + return (request, context, callback) => { responder(request, context).then( data => { @@ -52,9 +58,29 @@ export async function send(client: Client, data: object): Promise { await apiGateway.postToConnection(message).promise(); } +export namespace oauth2 { + const url = { + [Environment.Default]: "https://login.salesforce.com", + [Environment.Sandbox]: "https://test.salesforce.com", + }; + + export function configure(environment?: string): OAuth2 { + return new OAuth2(options(environment)); + } + + export function options(environment?: string): OAuth2Options { + return { + ...nconf.get("oauth"), + loginUrl: url[Environment.from(environment)], + }; + } +} + export namespace params { - export function get(request: Request, param: string): string | undefined { - return (request.queryStringParameters || {})[param]; + export function get(request: Request, param: string): string | undefined; + export function get(request: Request, param: string, value: string): string; + export function get(request: Request, param: string, value?: string): string | undefined { + return (request.queryStringParameters || {})[param] || value; } export function require(request: Request, param: string): string { diff --git a/src/commands/init.ts b/src/commands/init.ts index 8a774f7..3b4c23a 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,10 +1,11 @@ // tslint:disable: object-shorthand-properties-first import { Definition, Operator } from "@glide/runtime"; +import { Command } from "commander"; import jsforce from "jsforce"; import { Options } from "../lib"; -import { login } from "../oauth"; +import { Environment, login } from "../oauth"; import { json, string } from "../utilities"; interface Identifiers { @@ -18,9 +19,10 @@ interface Inflection { singular: string; } -export default async function init(origin: string, path: string = "glide.json"): Promise { - const { instance, schema } = createOptions(origin); - const connection = await login(instance); +export default async function init(origin: string, path: string, flags: Command): Promise { + const { instance, sandbox, schema } = createOptions(origin, flags); + const environment = sandbox ? Environment.Sandbox : Environment.Default; + const connection = await login(instance, environment); const sobjects = await connection.describeGlobal().then(({ sobjects }) => { return Promise.all(sobjects.map(({ name }) => connection.describe(name))); }); @@ -55,13 +57,15 @@ export default async function init(origin: string, path: string = "glide.json"): await json.write(path, { instance, + sandbox, schema, }); } -function createOptions(instance: string): Options { +function createOptions(instance: string, flags: Command): Options { return { instance, + sandbox: Boolean(flags.sandbox), schema: { mutations: {}, queries: {}, diff --git a/src/commands/serve.ts b/src/commands/serve.ts index 69a2226..f044b62 100644 --- a/src/commands/serve.ts +++ b/src/commands/serve.ts @@ -1,7 +1,11 @@ +import { EOL } from "os"; + +import boxen from "boxen"; +import chalk from "chalk"; import open from "open"; import glide, { Options } from "../lib"; -import { login } from "../oauth"; +import { Environment, login } from "../oauth"; import { display, json, isDevEnv } from "../utilities"; export interface Flags { @@ -15,8 +19,19 @@ export default async function serve(path: string = "glide.json", flags: Flags): const options = await configure(path); const listener = glide(options).listen(flags.port, () => { const address = display.address(listener); - - console.log(`server listening on ${address}`); + const message = [ + chalk`Config File: {underline ${path}}`, + chalk`GraphQL Server: {underline ${address}}`, + chalk`Salesforce Instance: {underline ${options.instance}}`, + ]; + + console.log( + boxen(message.join(EOL), { + float: "center", + margin: 3, + padding: 1, + }), + ); if (isDevEnv()) { open(address).then(browser => browser.unref()); @@ -36,8 +51,12 @@ export default async function serve(path: string = "glide.json", flags: Flags): async function configure(path: string): Promise { const options = await json.read(path); - return { - connection: isDevEnv() ? await login(options.instance) : null, - ...options, - }; + if (isDevEnv()) { + options.connection = await login( + options.instance, + options.sandbox ? Environment.Sandbox : Environment.Default, + ); + } + + return options; } diff --git a/src/lib.ts b/src/lib.ts index f3b5bb2..e9f8b00 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -34,6 +34,12 @@ export interface Options { */ instance: string; + /** + * If `true`, `https://test.salesforce.com` will be used in place of + * `https://login.salesforce.com` as a login url. + */ + sandbox?: boolean; + /** * A schema defintion that describes the shape of your Salesforce instance. */ diff --git a/src/main.ts b/src/main.ts index 5de94fa..50c943d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,7 +14,18 @@ export default function main(): void { commander .command("init [path]") .description("generates a glide.json file in the current directory") - .action(subcommand(init)); + .option("-s, --sandbox", "should be set if the given url points to a salesforce sandbox") + .action( + subcommand(async (origin, path, command) => { + let output = path; + + if (output == null) { + output = command.sandbox ? "glide.sandbox.json" : "glide.json"; + } + + await init(origin, output, command); + }), + ); commander .command("print [path]") diff --git a/src/oauth.ts b/src/oauth.ts index c40c0b1..a0f6de7 100644 --- a/src/oauth.ts +++ b/src/oauth.ts @@ -12,6 +12,11 @@ type Message = | Readonly<{ data: Session; type: MessageType.Authenticated }> | Readonly<{ data: string; type: MessageType.Initialize }>; +interface RefreshParams { + readonly environment?: Environment; + readonly token: string; +} + interface Refresh { readonly data?: Session; readonly errors?: [Pick]; @@ -32,11 +37,19 @@ const enum MessageType { Initialize = "INITIALIZE", } -export async function login(instance: string): Promise { +export const enum Environment { + Default = "default", + Sandbox = "sandbox", +} + +export async function login(instance: string, environment: Environment): Promise { const credentials = await loadCredentials(); if (credentials[instance]) { - return refreshToken(instance, credentials[instance]); + return refreshToken(instance, { + environment, + token: credentials[instance], + }); } return new Promise((resolve, reject) => { @@ -45,7 +58,7 @@ export async function login(instance: string): Promise { socket.on("open", () => { socket.send( JSON.stringify({ - data: null, + data: { environment }, type: MessageType.Initialize, }), ); @@ -102,10 +115,8 @@ async function loadCredentials(): Promise { return credentials; } -async function refreshToken(instance: string, token: string): Promise { - const [{ data, errors }, response] = await json.post(Endpoint.Refresh, { - token, - }); +async function refreshToken(instance: string, params: RefreshParams): Promise { + const [{ data, errors }, response] = await json.post(Endpoint.Refresh, params); return response.ok ? createConnection(instance, data!)