/
run.ts
152 lines (130 loc) Β· 5.6 KB
/
run.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
import chalk from 'chalk';
import readline from 'readline';
import {getFunctionNames} from '../apiutils.js';
import {getLocalScript, loadAPICredentials, script} from '../auth.js';
import {ClaspError} from '../clasp-error.js';
import {addScopeToManifest, isValidRunManifest} from '../manifest.js';
import {ERROR} from '../messages.js';
import {URL} from '../urls.js';
import {checkIfOnlineOrDie, getProjectSettings, parseJsonOrDie, spinner, stopSpinner} from '../utils.js';
interface CommandOption {
readonly nondev: boolean;
readonly params: string;
}
/**
* Executes an Apps Script function. Requires clasp login --creds.
* @param functionName {string} The function name within the Apps Script project.
* @param options.nondev {boolean} If we want to run the last deployed version vs the latest code.
* @param options.params {string} JSON string of parameters to be input to function.
* @see https://developers.google.com/apps-script/api/how-tos/execute
* @requires `clasp login --creds` to be run beforehand.
*/
export default async (functionName: string, options: CommandOption): Promise<void> => {
await checkIfOnlineOrDie();
await loadAPICredentials();
const {scriptId} = await getProjectSettings();
const devMode = !options.nondev; // Defaults to true
const {params: jsonString = '[]'} = options;
const parameters = parseJsonOrDie<string[]>(jsonString);
await isValidRunManifest();
// TODO COMMENT THIS. This uses a method that gives a HTML 404.
// await enableExecutionAPI();
// Pushes the latest code if in dev mode.
// We need to update the manifest before executing to:
// - Ensure the execution API is enabled.
// - Ensure we can run functions that were developed locally but not pushed.
if (devMode) {
console.log('Running in dev mode.');
// TODO enable this once we can properly await pushFiles
// await pushFiles(true);
}
await runFunction(functionName ?? (await getFunctionNames(script, scriptId)), parameters, scriptId, devMode);
};
/**
* Runs a function.
* @see https://developers.google.com/apps-script/api/reference/rest/v1/scripts/run#response-body
*/
const runFunction = async (functionName: string, parameters: string[], scriptId: string, devMode: boolean) => {
try {
// Load local credentials.
await loadAPICredentials(true);
const localScript = await getLocalScript();
spinner.start(`Running function: ${functionName}`);
const apiResponse = await localScript.scripts.run({
scriptId,
requestBody: {function: functionName, parameters, devMode},
});
stopSpinner();
if (!apiResponse?.data.done) {
throw new ClaspError(ERROR.RUN_NODATA, 0); // Exit gracefully in case localhost server spun up for authorize
}
const {error, response} = apiResponse.data;
// @see https://developers.google.com/apps-script/api/reference/rest/v1/scripts/run#response-body
if (response) {
console.log(response.result ?? chalk.red('No response.'));
} else if (error?.details) {
// @see https://developers.google.com/apps-script/api/reference/rest/v1/scripts/run#Status
const {errorMessage, errorType, scriptStackTraceElements} = error.details[0];
console.error(`${chalk.red('Exception:')}`, errorMessage, scriptStackTraceElements || []);
throw new ClaspError(errorType);
}
} catch (error) {
if (error instanceof ClaspError) {
throw error;
}
stopSpinner();
if (error) {
// TODO move these to logError when stable?
switch (error.code) {
case 401:
// The 401 is probably due to this error:
// "Error: Local client credentials unauthenticated. Check scopes/authorization.""
// This is probably due to the OAuth client not having authorized scopes.
console.log(`Hey! It looks like you aren't authenticated for the scopes required by this script.
Please enter the scopes by doing the following:
1. Open Your Script: ${URL.SCRIPT(scriptId)}
2. File > Project Properties > Scopes
3. Copy/Paste the list of scopes here:
~ Example ~
https://mail.google.com/
https://www.googleapis.com/auth/presentations
----(When you're done, press <Enter> 2x)----`);
readScopesFromStdinAndAddToManifest();
// We probably don't need to show the unauth error
// since we always prompt the user to fix this now.
// throw new ClaspError(ERROR.UNAUTHENTICATED_LOCAL);
break;
case 403:
throw new ClaspError(ERROR.PERMISSION_DENIED_LOCAL);
case 404:
throw new ClaspError(ERROR.EXECUTE_ENTITY_NOT_FOUND);
default:
throw new ClaspError(`(${error.code}) Error: ${error.message}`);
}
}
}
};
const readScopesFromStdinAndAddToManifest = () => {
// Example scopes:
// https://mail.google.com/
// https://www.googleapis.com/auth/presentations
// https://www.googleapis.com/auth/spreadsheets
const scopes: string[] = [];
const readlineInterface = readline.createInterface({input: process.stdin, output: process.stdout, prompt: ''});
const readScope: (input: string) => void = (input: string) => {
if (input === '') {
readlineInterface.close();
} else {
scopes.push(input);
}
};
const addToManifest = async (): Promise<void> => {
await addScopeToManifest(scopes);
const scopeCount = scopes.length;
console.log(`Added ${scopeCount} ${scopeCount === 1 ? 'scope' : 'scopes'} to your appsscript.json' oauthScopes`);
console.log('Please `clasp login --creds <file>` to log in with these new scopes.');
};
readlineInterface.prompt();
readlineInterface.on('line', readScope);
readlineInterface.on('close', addToManifest);
};