cover

Use GSuite Like a PRO

sorcererxw

Recently, there was a development requirement based on Google Office Suite, aiming to read and write Google Suite documents through the business backend. Generally speaking, using Google Suite SDK directly would be fine, but there are many capability limitations, and it can't even create Google Form. However, Google has packaged a lot of its capabilities into Google App Script (Google is too bad), so it is necessary to introduce GAS in the development.

Introduction to GAS

Why Use GAS?

However, since Google has already provided multiple SDKs for developers to use, why should we use Google App Script:

  • You can develop with Javascript or Typescript locally.

    Google App Script is essentially Javascript, there is no difference in syntax, it's just that gs runs under the GAS environment, where many specific interfaces can be invoked. You can also use Typescript for local writing, which will be automatically interpreted into AppsScript when uploaded.

  • More abundant interfaces

    As the "son" of GSuite, it can call some interfaces that are not in the SDK, such as creating Google Form.

    However, due to environmental constraints, the flexibility of interface usage is not as good as other platforms' SDKs

  • Can be deployed as a web service

    GAS can be deployed as a web service, allowing other programs to call it.

  • Eliminates the troublesome authentication operation for the caller

    If your own business service directly calls the GAS Web interface (provided that it is set to allow anonymous access during deployment), there is no need for additional authentication operations.

    If you are concerned about security issues, you can include authentication parameters in the query, and then authenticate them yourself in the GAS business.

Limitations of GAS

  • Not able to be engineered

    There is no concept of modules, functions within a single file can call each other, but cannot be called across files.

    Even if you can use tsc to compile multiple typescript files into a single js file, this approach cannot expose framework-specific methods such as doGet and doPost at the top-level. Therefore, even the compiled code cannot be recognized by the GAS framework.

  • No available Web frameworks

    Only two interfaces, doGet and doPost, can be defined and provided for the GAS framework to call. The remote caller only has two interfaces to operate.

    • GET https://script.google.com/macros/s/{scriptID}/exec
    • POST https://script.google.com/macros/s/{scriptID}/exec

    The specific logic depends on the query being called. Also, all returned data need to manually specify the MimeType, otherwise, it will be returned in the form of HTML.

  • Unable to debug locally

    Because there is no concept of modules, and therefore no way to call external libraries, Google APIs can only be called when deployed in the GAS runtime environment. For local development, Google only provides a @types/google-apps-script for convenient auto-prompting.

    Additionally, deployment also requires a local push first, followed by manual deployment via the web page, which significantly reduces development efficiency.

  • Synchronous Blocking

    All operations, whether they are document operations or network requests, are blocking. You need to wait for the return result before you can perform the next operation. This feature is both a disadvantage and an advantage.

Filling the gaps

Solving the issue of cross-file invocation

Managing multiple code files separately is the foundation of engineering, otherwise, the maintenance cost would be very high. Therefore, if you want to develop formal projects based on AppsScript, you need to solve the problem of cross-file invocation.

In Typescript, we generally use ES6's import module method for cross-file calls. Therefore, we only need to declare all the import used in each file at the top, and ensure that there are no circular references between the files. This way, we can calculate the module call topology graph. By replacing the import with the copied module code, we can compress this topology graph into the file.

// ------------
// 原始文件
// ------------

// index.ts
import * as A from './A'

function sayHello(){
	A.helloWorld()
}

// A.ts
import {foo} from './B'

export function helloWorld(){
	foo()
	console.log('Hello World!')
}

// B.ts

export function foo(){
}
// ------------
// 处理之后
// ------------

const B = (function () {
	function foo(){}
	B.foo = foo
})()

const foo = B.foo

const A = (function () {
  function helloWorld() {
		foo()
    console.log('Hello World!')
  }
  A.helloWorld = helloWorld
})();

function sayHello() {
  A.helloWorld()
}

The above is a simple demonstration of the desired effect, where the codes of multiple files can be merged in this way. However, there are other considerations in real situations. Here are a few main points:

  • To prevent module circular references, pre-checking is required before interpretation, you can refer to madge.
  • Need to prevent the use of require

Now Google AppsScript is paired with a tool clasp, which can quickly authenticate and upload deployment code. Uploading code through this tool will automatically convert JavaScript and TypeScript into gs files. For TypeScript files, you can see Compiled using ts2gas at the header of the converted file, indicating that clasp uses ts2gas to compile TypeScript.

Go take a look at the implementation of ts2gas, you'll see in the header comments:

/**
 * Transpiles a TypeScript file into a valid Apps Script file.
 * @param {string} source The TypeScript source code as a string.
 * @param {ts.TranspileOptions} transpileOptions custom transpile options.
 * @see https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
 */

As you can see, ts2gas is not really magical, it just uses the Compiler-API provided by typescript to implement ts compilation. By using the compiled results and adding a bit of customized code, it can generate gs files (as mentioned before, gs is actually js).

So, can we also make the compiled gs meet our expected results based on ts2gas? Let's first take a look at how ts2gas is implemented.

const ts2gas = (source: string, transpileOptions: ts.TranspileOptions = {}) => {
	transpileOptions = deepAssign({},  // safe to mutate
    defaults,  // default (overridable)
    transpileOptions,  // user override
    statics,  // statics
  );
	
	// Transpile (cf. https://www.typescriptlang.org/docs/handbook/compiler-options.html)
  const result = ts.transpileModule(source, transpileOptions);

  // # Clean up output (multiline string)
  let output = result.outputText;
  
	const pjson = require('../package.json');  // ugly hack

  // Include an exports object in all files.
  output = `// Compiled using ${pjson.name} ${pjson.version} (TypeScript ${ts.version})
var exports = exports || {};
var module = module || { exports: exports };
${output}`;

	return output;
}

The above is the core code of ts2gas, which compiles the original code through ts's transpileModule. The function allows the caller to pass in their own transpileOptions to override the defaultOption, but there is still a staticsOption to forcibly ensure the accuracy of certain features. By adjusting the staticsOption, restrictions such as preventing the use of require can be imposed, throwing errors during compilation.