forked from abkarino/FirestoreGoogleAppsScript
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Util.ts
183 lines (165 loc) · 6.03 KB
/
Util.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
class Util_ {
/**
* RegEx test for root path references. Groups relative path for extraction.
*/
static get regexPath(): RegExp {
return /^projects\/.+?\/databases\/\(default\)\/documents\/(.+\/.+)$/;
}
/**
* RegEx test for testing for binary data by checking for non-printable characters.
* Parsing strings for binary data is completely dependent on the data being sent over.
*/
static get regexBinary(): RegExp {
// eslint-disable-next-line no-control-regex
return /[\x00-\x08\x0E-\x1F]/;
}
/**
* RegEx test for finding and capturing milliseconds.
* Apps Scripts doesn't support RFC3339 Date Formats, nanosecond precision must be trimmed.
*/
static get regexDatePrecision(): RegExp {
return /(\.\d{3})\d+/;
}
/**
* Checks if a number is an integer.
*
* @param {value} n value to check
* @return {boolean} true if value can be coerced into an integer, false otherwise
*/
static isInt(n: number): boolean {
return n % 1 === 0;
}
/**
* Check if a value is a valid number.
*
* @param {value} val value to check
* @return {boolean} true if a valid number, false otherwise
*/
static isNumeric(val: number): boolean {
return Number(parseFloat(val.toString())) === val;
}
/**
* Check if a value is of type Number but is NaN.
* This check prevents seeing non-numeric values as NaN.
*
* @param {value} value value to check
* @return {boolean} true if NaN, false otherwise
*/
static isNumberNaN(value: any): boolean {
return typeof value === 'number' && isNaN(value);
}
/**
* Gets collection of documents with the given path
*
* @param {string} path Collection path
* @return {array} Collection of documents
*/
static getCollectionFromPath(path: string): [string, string] {
return this.getColDocFromPath(path, false);
}
/**
* Gets document with the given path
*
* @param {string} path Document path
* @return {object} Document object
*/
static getDocumentFromPath(path: string): [string, string] {
return this.getColDocFromPath(path, true);
}
/**
* Gets collection or document with the given path
*
* @param {string} path Document/Collection path
* @return {array|object} Collection of documents or a single document
*/
static getColDocFromPath(path: string, isDocument: any): [string, string] {
// Path defaults to empty string if it doesn't exist. Remove insignificant slashes.
const splitPath = (path || '').split('/').filter(function (p) {
return p;
});
const len = splitPath.length;
this.cleanParts(splitPath);
// Set item path to document if isDocument, otherwise set to collection if exists.
// This works because path is always in the format of "collection/document/collection/document/etc.."
const item = len && (len & 1) ^ isDocument ? splitPath.splice(len - 1, 1)[0] : '';
// Remainder of path is in splitPath. Put back together and return.
return [splitPath.join('/'), item];
}
/**
* Gets document names from list of documents
*
* @param {string} path Relative collection path
* @param {FirestoreAPI.Document[]} docs Array of documents
* @return {string[]} of names
*/
static stripBasePath(path: string, docs: FirestoreAPI.Document[]): string[] {
return docs.map((doc: FirestoreAPI.Document) => {
const ref: string = doc.name!.match(Util_.regexPath)![1]; // Gets the doc name field and extracts the relative path
return ref.substr(path.length + 1); // Skip over the given path to gain the ID values
});
}
/**
* Validates Collection and Document names
*
* @see {@link https://firebase.google.com/docs/firestore/quotas#collections_documents_and_fields Firestore Limits}
* @param {array} parts Array of strings representing document path
* @return {array} of URI Encoded path names
* @throws {Error} Validation errors if it doesn't meet API guidelines
*/
static cleanParts(parts: string[]): string[] {
return parts.map(function (part, i) {
const type = i & 1 ? 'Collection' : 'Document';
if (part === '.' || part === '..') {
throw new TypeError(type + ' name cannot solely consist of a single period (.) or double periods (..)');
}
if (part.indexOf('__') === 0 && part.endsWith('__')) {
throw new TypeError(type + ' name cannot be a dunder name (begin and end with double underscores)');
}
return encodeURIComponent(part);
});
}
/**
* Splits up path to be cleaned
*
* @param {string} path to be cleaned
* @return {string} path that is URL-safe
*/
static cleanPath(path: string): string {
return this.cleanParts(path.split('/')).join('/');
}
static parameterize(obj: any, encode = true): string {
const process = encode ? encodeURI : (s: string): string => s;
return Object.entries<string>(obj)
.map(([k, v]) => `${process(k)}=${process(v)}`)
.join('&');
}
/**
* Create a new unique document id
*
* @return {string} a new unique document id
* @link https://github.com/firebase/firebase-js-sdk/blob/34ad43cc2a9863f7ac326c314d9539fcbc1f0913/packages/firestore/src/util/misc.ts#L28
*/
static newId(): string {
// Alphanumeric characters
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
// The largest byte value that is a multiple of `char.length`.
const maxMultiple = Math.floor(256 / chars.length) * chars.length;
let autoId = '';
const targetLength = 20;
while (autoId.length < targetLength) {
const nBytes = 40;
const bytes = new Uint8Array(nBytes);
for (let i = 0; i < nBytes; i++) {
bytes[i] = Math.floor(Math.random() * 256);
}
for (let i = 0; i < bytes.length; ++i) {
// Only accept values that are [0, maxMultiple), this ensures they can
// be evenly mapped to indices of `chars` via a modulo operation.
if (autoId.length < targetLength && bytes[i] < maxMultiple) {
autoId += chars.charAt(bytes[i] % chars.length);
}
}
}
return autoId;
}
}