-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Initial setup and source code * Add docs to Readme
- Loading branch information
Showing
8 changed files
with
3,287 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"presets": ["@babel/preset-env"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{ | ||
"root": true, | ||
"parser": "babel-eslint", | ||
"extends": ["airbnb-base", "plugin:prettier/recommended"], | ||
"plugins": ["babel", "prettier"], | ||
"env": { | ||
"browser": true, | ||
"es6": true | ||
}, | ||
"globals": { | ||
"google": false, | ||
"alert": false, | ||
"css": true | ||
}, | ||
"parserOptions": { | ||
"ecmaVersion": 9, | ||
"sourceType": "module", | ||
"ecmaFeatures": { | ||
"jsx": true | ||
} | ||
}, | ||
"rules": { | ||
"prettier/prettier": "error", | ||
"camelcase": "warn", | ||
"import/prefer-default-export": "warn", | ||
"import/no-extraneous-dependencies": "warn", | ||
"prefer-object-spread": "warn" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,10 @@ | ||
# mac | ||
.DS_Store | ||
|
||
# build | ||
build/ | ||
package*/ | ||
|
||
# Logs | ||
logs | ||
*.log | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"singleQuote": true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
# gas-client | ||
|
||
A client-side utility class that uses promises to call server-side Google Apps Script functions. This is a user-friendly wrapper of [google.script.run](https://developers.google.com/apps-script/guides/html/reference/run). | ||
|
||
It can also optionally be used in local development and is designed to interact with the [Google Apps Script Dev Server](https://github.com/enuchi/Google-Apps-Script-Webpack-Dev-Server) used in the [React / Google Apps Script](https://github.com/enuchi/React-Google-Apps-Script) project. | ||
|
||
--- | ||
|
||
## Installation | ||
|
||
Install | ||
```bash | ||
> npm install gas-client | ||
# or | ||
> yarn add gas-client | ||
``` | ||
|
||
```javascript | ||
import Server from 'gas-client'; | ||
const { serverFunctions } = new Server(); | ||
|
||
// We now have access to all our server functions, which return promises | ||
serverFunctions | ||
.addSheet(sheetTitle) | ||
.then((response) => doSomething(response)) | ||
.catch((err) => handleError(err)); | ||
``` | ||
|
||
### Development mode | ||
|
||
To use with [Google Apps Script Dev Server](https://github.com/enuchi/Google-Apps-Script-Webpack-Dev-Server), pass in a config object with `allowedDevelopmentDomains` indicating the localhost port you are using. This setting will be ignored in production (see below for more details). | ||
|
||
```javascript | ||
import Server from 'gas-client'; | ||
|
||
const { serverFunctions } = new Server({ | ||
allowedDevelopmentDomains: 'https://localhost:3000', | ||
}); | ||
|
||
serverFunctions | ||
.addSheet(sheetTitle) | ||
.then((response) => doSomething(response)) | ||
.catch((err) => handleError(err)); | ||
``` | ||
|
||
--- | ||
|
||
## How to use | ||
|
||
### Using the gas-client utility class | ||
|
||
The `gas-client` file lets you use promises to call and handle responses from the server, instead of using `google.script.run`: | ||
|
||
```javascript | ||
// Google's client-side utility "google.script.run" works like this: | ||
google.script.run | ||
.withSuccessHandler((response) => doSomething(response)) | ||
.withFailureHandler((err) => handleError(err)) | ||
.addSheet(sheetTitle); | ||
``` | ||
|
||
```javascript | ||
// With this package we can now do this: | ||
import Server from 'gas-client'; | ||
const { serverFunctions } = new Server(); | ||
|
||
// We now have access to all our server functions, which return promises | ||
serverFunctions | ||
.addSheet(sheetTitle) | ||
.then((response) => doSomething(response)) | ||
.catch((err) => handleError(err)); | ||
|
||
// Or we can use async/await syntax: | ||
async () => { | ||
try { | ||
const response = await serverFunctions.addSheet(sheetTitle); | ||
doSomething(response); | ||
} catch (err) { | ||
handleError(err); | ||
} | ||
}; | ||
``` | ||
|
||
Now we can use familiar Promises in our client-side code and have easy access to all server functions. | ||
|
||
--- | ||
|
||
## API | ||
|
||
The config object takes: | ||
`allowedDevelopmentDomains`: A config to specifiy which domains are permitted for communication with Google Apps Script Webpack Dev Server development tool. This is a security setting, and if not specified, will block functionality in development. | ||
|
||
`allowedDevelopmentDomains` will accept either a space-separated string of allowed subdomains, e.g. `'https://localhost:3000 https://localhost:8080'` (notice no trailing slashes); or a function that takes in the requesting origin and should return `true` to allow communication, e.g. `(origin) => /localhost:\d+$/.test(origin);` | ||
|
||
### Production mode | ||
|
||
In the normal Google Apps Script production environment, `new Server()` will have one available method: | ||
|
||
- `serverFunctions`: an object containing all publicly exposed server functions (see example above). | ||
|
||
Note that the `allowedDevelopmentDomains` configuration will be ignored in production, so the same code can and should be used for development and production. | ||
|
||
### Development mode | ||
|
||
Development mode for the `gas-client` helper class will be run when: | ||
|
||
1. the `google` client API cannot be found, i.e. an error is thrown with the message "ReferenceError: google is not defined", and | ||
|
||
2. a `process.env.NODE_ENV` variable is set to `'development'`. [webpack.DefinePlugin](https://webpack.js.org/plugins/define-plugin/) can be set this up like this: | ||
|
||
```javascript | ||
plugins: [ | ||
// ..., | ||
new webpack.DefinePlugin({ | ||
'process.env': JSON.stringify({ NODE_ENV: 'development' }), | ||
}), | ||
]; | ||
``` | ||
|
||
Calling `new Server({ allowedDevelopmentDomains })` will create an instance with the following method in development mode: | ||
|
||
- `serverFunctions`: a proxy object, used for development purposes, that mimics calling `google.script.run`. It will dispatch a message to the parent iframe (our custom Dev Server), which will call an app that actually interacts with the `google.script.run` API. Development mode will also handle the response and resolve or reject based on the response type. See the implementation for details on the event signature. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
{ | ||
"name": "gas-client", | ||
"version": "0.1.0", | ||
"description": "A client-side utility class that can call server-side Google Apps Script functions", | ||
"main": "build/index.js", | ||
"files": [ | ||
"build", | ||
"src" | ||
], | ||
"repository": "https://github.com/enuchi/gas-client.git", | ||
"author": "Elisha Nuchi", | ||
"license": "MIT", | ||
"scripts": { | ||
"prepack": "yarn build", | ||
"build": "babel src/index.js -d build" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/enuchi/gas-client/issues" | ||
}, | ||
"homepage": "https://github.com/enuchi/gas-client#readme", | ||
"keywords": [ | ||
"Goole", | ||
"Apps", | ||
"Script", | ||
"GAS", | ||
"Client" | ||
], | ||
"devDependencies": { | ||
"@babel/cli": "^7.10.5", | ||
"@babel/core": "^7.10.5", | ||
"@babel/preset-env": "^7.10.4", | ||
"babel-eslint": "^10.1.0", | ||
"eslint": "^7.5.0", | ||
"eslint-config-airbnb-base": "^14.2.0", | ||
"eslint-config-prettier": "^6.11.0", | ||
"eslint-plugin-babel": "^5.3.1", | ||
"eslint-plugin-import": "^2.22.0", | ||
"eslint-plugin-prettier": "^3.1.4", | ||
"prettier": "^2.0.5" | ||
}, | ||
"dependencies": { | ||
"uuid": "^8.2.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { v4 as uuidv4 } from 'uuid'; | ||
|
||
/** | ||
* Util that returns true if allowedDevelopmentDomains matches origin | ||
* @param {string|function} allowedDevelopmentDomains either a string of space-separated allowed subdomains or a function that accepts the origin as an argument and returns true if permitted | ||
* @param {string} origin the target origin subdomain to compare against | ||
*/ | ||
const checkAllowList = (allowedDevelopmentDomains, origin) => { | ||
if (typeof allowedDevelopmentDomains === 'string') { | ||
return allowedDevelopmentDomains | ||
.split(' ') | ||
.some((permittedOrigin) => permittedOrigin === origin); | ||
} | ||
if (typeof allowedDevelopmentDomains === 'function') { | ||
return allowedDevelopmentDomains(origin) === true; | ||
} | ||
return false; | ||
}; | ||
|
||
export default class Server { | ||
/** | ||
* Accepts a single `config` object | ||
* @param {object} [config] An optional config object for use in development. | ||
* @param {string|function} [config.allowedDevelopmentDomains] An optional config to specify which domains are permitted for communication with Google Apps Script Webpack Dev Server development tool. This is a security setting, and if not specified, this will block functionality in development. Will accept either a space-separated string of allowed subdomains, e.g. `https://localhost:3000 http://localhost:3000` (notice no trailing slash); or a function that takes in the requesting origin should return `true` to allow communication, e.g. `(origin) => /localhost:\d+$/.test(origin)` | ||
*/ | ||
constructor(config = {}) { | ||
// skip the reserved names: https://developers.google.com/apps-script/guides/html/reference/run | ||
const ignoredFunctionNames = [ | ||
'withFailureHandler', | ||
'withLogger', | ||
'withSuccessHandler', | ||
'withUserObject', | ||
]; | ||
|
||
this.serverFunctions = {}; | ||
|
||
try { | ||
// get the names of all of our publicly accessible server functions | ||
const functionNames = Object.keys(google.script.run).filter( | ||
// filter out the reserved names -- we don't want those | ||
(functionName) => !ignoredFunctionNames.includes(functionName) | ||
); | ||
|
||
// attach Promise-based functions to the serverFunctions property | ||
functionNames.forEach((functionName) => { | ||
this.serverFunctions[functionName] = (...args) => | ||
new Promise((resolve, reject) => { | ||
google.script.run | ||
.withSuccessHandler(resolve) | ||
.withFailureHandler(reject) | ||
[functionName](...args); | ||
}); | ||
}); | ||
} catch (err) { | ||
if ( | ||
err.toString() === 'ReferenceError: google is not defined' && | ||
process && | ||
process.env && | ||
process.env.NODE_ENV === 'development' | ||
) { | ||
// we'll store and access the resolve/reject functions here by id | ||
window.gasStore = {}; | ||
|
||
// set up the message 'receive' handler | ||
const receiveMessageHandler = (event) => { | ||
const { allowedDevelopmentDomains } = config; | ||
|
||
// check the allow list for the receiving origin | ||
const allowOrigin = checkAllowList( | ||
allowedDevelopmentDomains, | ||
event.origin | ||
); | ||
if (!allowOrigin) return; | ||
|
||
// we only care about the type: 'RESPONSE' messages here | ||
if (event.data.type !== 'RESPONSE') return; | ||
|
||
const { response, status, id } = event.data; | ||
|
||
// look up the saved resolve and reject funtions in our global store based | ||
// on the response id, and call the function depending on the response status | ||
const { resolve, reject } = window.gasStore[id]; | ||
|
||
if (status === 'ERROR') { | ||
reject(response); | ||
} | ||
resolve(response); | ||
}; | ||
|
||
window.addEventListener('message', receiveMessageHandler, false); | ||
|
||
const handler = { | ||
get(target, functionName) { | ||
const id = uuidv4(); | ||
const promise = new Promise((resolve, reject) => { | ||
// store the new Promise's resolve and reject | ||
window.gasStore[id] = { resolve, reject }; | ||
}); | ||
return (...args) => { | ||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage | ||
window.parent.postMessage( | ||
{ | ||
type: 'REQUEST', | ||
id, | ||
functionName, | ||
args: [...args], | ||
}, | ||
// only send messages to our dev server, which should be running on the same origin | ||
window.location.origin | ||
); | ||
return promise; | ||
}; | ||
}, | ||
}; | ||
this.serverFunctions = new Proxy({}, handler); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.