Skip to content

Commit 38b94d6

Browse files
committed
Convert project to V8 Runtime with Classes. #83 #56
Convert JavaScript to Typescript and create custom typings. #64 Added Typings from gapi.client, google-apps-script, and jsonwebtoken. Generated typings from Firestore Discovery URL from google-api-typings-generator. Embraced ESLint and Prettier for code style. Integrated Unit Tests utilizing GSUnit. Added a plethora of badges and updated documentation. Create workaround for duplicate "console" definitions (DefinitelyTyped/DefinitelyTyped#32585).
1 parent ab2a643 commit 38b94d6

37 files changed

+6323
-1158
lines changed

.claspignore

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
LICENSE
2-
!appsscript.json
32
*.md
4-
package*.json
3+
*.json
4+
!appsscript.json
5+
node_modules/**
6+
node_modules/**/.*/**
7+
node_modules/**/.*
8+
typings/**
9+
typings/**/.*/**
10+
typings/**/.*

.github/CONTRIBUTING.md

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,30 @@
22
Contributions are welcome! To contribute, fork this repository and create a pull request with your changes.
33

44
#### Pull requests
5-
* Please create an issue before creating a pull request. Your changes are more likely to be pulled in after we discuss implementation, scope, etc. in an issue.
5+
* Please create (or link to) an issue before creating a pull request. Your changes are more likely to be pulled in after we discuss implementation, scope, etc. in the issue.
66
* Please be responsive to change requests on your pull request.
77

88
#### Checklist for your code
9-
* Have you tested the code yourself?
9+
- [ ] Have you all the appropriate dependencies (after `git clone`)?
10+
* Install all packages from `package.json` with a bare `npm install`.
11+
- [ ] Have you tested the code yourself?
1012
* Install [clasp](https://developers.google.com/apps-script/guides/clasp) `npm install --global @google/clasp`
1113
* Log into your Google Account `clasp login`
12-
* Create/Clone Apps Script Project
13-
* Create a Test.js file which creates a Firestore instance (via. `getFirestore()`)
14-
* Debug/Test your code against your database
15-
* Have you [documented your functions and their parameters and return values with JSDoc](http://usejsdoc.org/about-getting-started.html)?
16-
* Have you made all functions that an end user of the library should not see private? (You can make a function private by adding `_` to the end of its name, as in `publicFunction()` and `privateFunction_()`).
17-
* Add new "global" functions to `package.json`
18-
* Does your code follow the [Standard Javascript Style Guide](https://github.com/standard/standard)?
19-
* Install [standard](https://github.com/standard/standard) `npm install --global standard`
20-
* Check code against rules `standard --verbose`
21-
* Autofix most issues `standard --fix`
14+
* Create/Clone Apps Script Project to link and set file extension to "ts". Example `.clasp.json` file:
15+
```javascript
16+
{
17+
"scriptId": "1yIYw-your-scriptID-here-las12jsA2sd",
18+
"fileExtension": "ts"
19+
}
20+
```
21+
* Add storable credentials to your script to connect to your Firestore database.
22+
* Add test methods to `Test.ts` file which validates your change.
23+
* Debug/Test your code against your database.
24+
- [ ] Have you [documented your functions and their parameters and return values with JSDoc](http://usejsdoc.org/about-getting-started.html)?
25+
- [ ] Have you modernized the code to run with the new [V8 Runtime](https://developers.google.com/apps-script/guides/v8-runtime)?
26+
- [ ] Have you privatized or encapsulated all intended functions in a class?
27+
* Global functions can be privatized by appending the "`_`" to the function name.
28+
- [ ] Have you ensured all the appropriate Typescript Definitions are available?
29+
- [ ] Does your code follow the [Prettier Javascript Style](https://prettier.io/docs/en/install.html)?
30+
* Check syntax and style in one go with `npm run lint`.
31+
* Autofix most issues with `npm run fix`.

.github/README.md

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
# Firestore for Google Apps Scripts
22

3-
[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
3+
![GitHub release (latest by date)](https://img.shields.io/github/v/release/grahamearley/FirestoreGoogleAppsScript)
4+
[![Google Apps Script](https://img.shields.io/badge/google%20apps%20script-v8-%234285f4)](https://developers.google.com/apps-script/guides/v8-runtime)
5+
[![TypeScript](https://img.shields.io/badge/typescript-3.9.3-%23294E80)](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html)
46
[![clasp](https://img.shields.io/badge/built%20with-clasp-4285f4.svg)](https://github.com/google/clasp)
7+
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)
8+
[![GitHub pull requests](https://img.shields.io/github/issues-pr/grahamearley/FirestoreGoogleAppsScript)](https://github.com/grahamearley/FirestoreGoogleAppsScript/pulls)
9+
[![GitHub issues](https://img.shields.io/github/issues/grahamearley/FirestoreGoogleAppsScript)](https://github.com/grahamearley/FirestoreGoogleAppsScript/issues)
10+
![Tests](https://img.shields.io/endpoint?url=https%3A%2F%2Fscript.google.com%2Fmacros%2Fs%2FAKfycbyvSXmyngRI1CPvMzIm7opGgB_19fFuvLC6AakL5ezxjKsIT6I%2Fexec)
511

612
### A Google Apps Script library for accessing Google Cloud Firestore.
713

@@ -10,6 +16,8 @@ This library allows a user (or service account) to authenticate with Firestore a
1016

1117
Read how this project was started [here](http://grahamearley.website/blog/2017/10/18/firestore-in-google-apps-script.html).
1218

19+
As of **v27**, this project has been updated to use the [GAS V8 runtime](https://developers.google.com/apps-script/guides/v8-runtime) with [Typescript](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html)! This introduces a number of [breaking changes](#breaking-changes).
20+
1321
## Installation
1422
In the Google online script editor, select the `Resources` menu item and choose `Libraries...`. In the "Add a library" input box, enter `1VUSl4b1r1eoNcRWotZM3e87ygkxvXltOgyDZhixqncz9lQ3MjfT1iKFw` and click "Add." Choose the most recent version number.
1523

@@ -28,10 +36,10 @@ To make a service account,
2836
5. When you press "Create," your browser will download a `.json` file with your private key (`private_key`), service account email (`client_email`), and project ID (`project_id`). Copy these values into your Google Apps Script — you'll need them to authenticate with Firestore.
2937

3038
#### Create a test document in Firestore from your script
31-
Now, with your service account client email address `email`, private key `key`, project ID `projectId`, and Firestore API version, we will authenticate with Firestore to get our `Firestore` object. To do this, get the `Firestore` object from the library:
39+
Now, with your service account client email address `email`, private key `key`, project ID `projectId`, we will authenticate with Firestore to get our `Firestore` object. To do this, get the `Firestore` object from the library:
3240

3341
```javascript
34-
var firestore = FirestoreApp.getFirestore(email, key, projectId, "v1");
42+
const firestore = FirestoreApp.getFirestore(email, key, projectId);
3543
```
3644

3745
Using this Firestore instance, we will create a Firestore document with a field `name` with value `test!`. Let's encode this as a JSON object:
@@ -42,59 +50,87 @@ const data = {
4250
}
4351
```
4452

45-
We can choose to create a document in collection called `FirstCollection` without an ID:
53+
We can choose to create a document in collection called `FirstCollection` without a name (Firestore will generate one):
4654

4755
```javascript
48-
firestore.createDocument("FirstCollection", data)
56+
firestore.createDocument("FirstCollection", data);
4957
```
5058

5159
Alternatively, we can create the document in the `FirstCollection` collection called `FirstDocument`:
5260
```javascript
53-
firestore.createDocument("FirstCollection/FirstDocument", data)
61+
firestore.createDocument("FirstCollection/FirstDocument", data);
5462
```
5563

56-
To update the document at this location, we can use the `updateDocument` function:
64+
To update (overwrite) the document at this location, we can use the `updateDocument` function:
5765
```javascript
58-
firestore.updateDocument("FirstCollection/FirstDocument", data)
66+
firestore.updateDocument("FirstCollection/FirstDocument", data);
5967
```
6068

61-
**Note:** Although you can call `updateDocument` without using `createDocument` to create the document, any documents in your path will not be created and thus you can only access the document by using the path explicitly.
69+
To update only specific fields of a document at this location, we can set the `mask` parameter to `true`:
70+
```javascript
71+
firestore.updateDocument("FirstCollection/FirstDocument", data, true);
72+
```
6273

63-
You can retrieve your data by calling the `getDocument` function:
74+
You can retrieve documents by calling the `getDocument` function:
6475

6576
```javascript
66-
const dataWithMetadata = firestore.getDocument("FirstCollection/FirstDocument")
77+
const documentWithMetadata = firestore.getDocument("FirstCollection/FirstDocument");
78+
const storedObject = documentWithMetadata.obj;
6779
```
6880

6981
You can also retrieve all documents within a collection by using the `getDocuments` function:
7082

7183
```javascript
72-
const allDocuments = firestore.getDocuments("FirstCollection")
84+
const allDocuments = firestore.getDocuments("FirstCollection");
7385
```
7486

7587
You can also get specific documents by providing an array of document names
7688

7789
```javascript
78-
const someDocuments = firestore.getDocuments("FirstCollection", ["Doc1", "Doc2", "Doc3"])
90+
const someDocuments = firestore.getDocuments("FirstCollection", ["Doc1", "Doc2", "Doc3"]);
7991
```
8092

81-
82-
If more specific queries need to be performed, you can use the `query` function followed by an `execute` invocation to get that data:
93+
If more specific queries need to be performed, you can use the `query` function followed by an `Execute` invocation to get that data:
8394

8495
```javascript
85-
const allDocumentsWithTest = firestore.query("FirstCollection").where("name", "==", "Test!").execute()
96+
const allDocumentsWithTest = firestore.query("FirstCollection").Where("name", "==", "Test!").Execute();
97+
```
98+
99+
The `Where` function can take other operators too: `==`, `<`, `<=`, `>`, `>=`, `contains`, `contains_any`, `in`.
100+
101+
Queries looking for `null` values can also be given:
102+
```javascript
103+
const allDocumentsNullNames = firestore.query("FirstCollection").Where("name", null).Execute();
104+
```
105+
106+
Query results can be ordered:
107+
```javascript
108+
const allDocumentsNameAsc = firestore.query("FirstCollection").OrderBy("name").Execute();
109+
const allDocumentsNameDesc = firestore.query("FirstCollection").OrderBy("name", "desc").Execute();
110+
```
111+
112+
To limit, offset, or just select a range of results:
113+
```javascript
114+
const documents2_3_4_5 = firestore.query("FirstCollection").Limit(4).Offset(2).Execute();
115+
const documents3_4_5_6 = firestore.query("FirstCollection").Range(3, 7).Execute();
86116
```
87117

88-
See other library methods and details [in the wiki](https://github.com/grahamearley/FirestoreGoogleAppsScript/wiki/).
118+
See other library methods and details [in the wiki](/grahamearley/FirestoreGoogleAppsScript/wiki/).
89119

90120
### Breaking Changes
91-
* v23: When retrieving documents the createTime and updateTime document properties are JS Date objects and not Timestamp Strings.
92-
* v16: **Removed:** `createDocumentWithId(documentId, path, fields)`
121+
* **v27:** Library rewritten with Typescript and Prettier.
122+
* Query function names have been capitalized (`Select`, `Where`, `OrderBy`, `Limit`, `Offset`, `Range`).
123+
* **All functions return `Document` or `Document[]` types directly from Firebase. Use `document.obj` to extract the raw object.**
124+
* Undo breaking change from v23. `document.createTime` and `document.updateTime` will remain as timestamped strings. However `document.created`, `document.updated`, and `document.read` are Date objects.
125+
* **v23:** When retrieving documents the createTime and updateTime document properties are JS Date objects and not Timestamp Strings.
126+
* **v16:** **Removed:** `createDocumentWithId(documentId, path, fields)`
93127
> Utilize `createDocument(path + '/' + documentId, fields)` instead to create a document with a specific ID.
94128
95129
## Contributions
96-
Contributions are welcome — send a pull request! This library is a work in progress. See [here](https://github.com/grahamearley/FirestoreGoogleAppsScript/blob/master/.github/CONTRIBUTING.md) for more information on contributing.
130+
Contributions are welcome — send a pull request! See [here](/grahamearley/FirestoreGoogleAppsScript/blob/master/.github/CONTRIBUTING.md) for more information on contributing.
97131

98132
After cloning this repository, you can push it to your own private copy of this Google Apps Script project to test it yourself. See [here](https://github.com/google/clasp) for directions on using `clasp` to develop App Scripts locally.
133+
Install all packages from `package.json` with a bare `npm install`.
134+
99135

100-
If you want to view the source code directly on Google Apps Script, where you can make a copy for yourself to edit, click [here](https://script.google.com/d/1VUSl4b1r1eoNcRWotZM3e87ygkxvXltOgyDZhixqncz9lQ3MjfT1iKFw/edit?usp=sharing).
136+
If you want to view the source code directly on Google Apps Script, where you can make a copy for yourself to edit, click [here](https://script.google.com/d/1VUSl4b1r1eoNcRWotZM3e87ygkxvXltOgyDZhixqncz9lQ3MjfT1iKFw/edit).

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
gapps.config.json
2-
appsscript.json
31
.clasp.json
42
.vs
3+
gapps.config.json
4+
node_modules

Auth.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Auth token is formatted to {@link https://developers.google.com/identity/protocols/oauth2/service-account#authorizingrequests}
3+
*
4+
* @private
5+
* @param email the database service account email address
6+
* @param key the database service account private key
7+
* @param authUrl the authorization url
8+
* @returns {string} the access token needed for making future requests
9+
*/
10+
class Auth {
11+
email: string;
12+
key: string;
13+
authUrl: string;
14+
scope: string;
15+
jwtHeaderBase64_: string;
16+
17+
constructor(email: string, key: string) {
18+
this.email = email;
19+
this.key = key;
20+
21+
// TODO: Fix below variables to be static private when V8 allows for it.
22+
// @see {@link https://issuetracker.google.com/issues/150896358 Google Issue Tracker}
23+
this.authUrl = 'https://oauth2.googleapis.com/token';
24+
this.scope = 'https://www.googleapis.com/auth/datastore';
25+
this.jwtHeaderBase64_ = Utilities.base64EncodeWebSafe(JSON.stringify(this.jwtHeader_));
26+
}
27+
28+
/**
29+
* Fetch the access token
30+
*
31+
* @returns {string} The generated access token string
32+
*/
33+
get accessToken(): string {
34+
const request = new Request(this.authUrl, '', this.options_).post<TokenResponse>();
35+
return request.access_token;
36+
}
37+
38+
/**
39+
* Creates the JSON Web Token for OAuth 2.0
40+
*
41+
* @returns {string} JWT to utilize
42+
*/
43+
get createJwt_(): string {
44+
const jwtClaimBase64 = Utilities.base64EncodeWebSafe(JSON.stringify(this.jwtPayload_));
45+
const signatureInput = `${this.jwtHeaderBase64_}.${jwtClaimBase64}`;
46+
const signature: number[] = Utilities.computeRsaSha256Signature(signatureInput, this.key);
47+
return `${signatureInput}.${Utilities.base64EncodeWebSafe(signature)}`;
48+
}
49+
50+
get jwtPayload_(): JwtClaim {
51+
const seconds = ~~(new Date().getTime() / 1000);
52+
return {
53+
iss: this.email,
54+
scope: this.scope,
55+
aud: this.authUrl,
56+
exp: seconds + 3600, // Expiry set to 1 hour (maximum)
57+
iat: seconds,
58+
};
59+
}
60+
61+
get jwtHeader_(): JwtHeader {
62+
return {
63+
alg: 'RS256',
64+
typ: 'JWT',
65+
};
66+
}
67+
68+
get options_(): RequestOptions {
69+
const payload = {
70+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
71+
assertion: this.createJwt_,
72+
};
73+
return { payload: Util_.parameterize(payload, false) };
74+
}
75+
}

Authenticate.js

Lines changed: 0 additions & 61 deletions
This file was deleted.

Delete.js

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)