mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) move home server into core
Summary: This moves enough server material into core to run a home server. The data engine is not yet incorporated (though in manual testing it works when ported). Test Plan: existing tests pass Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2552
This commit is contained in:
11
app/plugin/CustomSectionAPI-ti.ts
Normal file
11
app/plugin/CustomSectionAPI-ti.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import * as t from "ts-interface-checker";
|
||||
// tslint:disable:object-literal-key-quotes
|
||||
|
||||
export const CustomSectionAPI = t.iface([], {
|
||||
"createSection": t.func("void", t.param("inlineTarget", "RenderTarget")),
|
||||
});
|
||||
|
||||
const exportedTypeSuite: t.ITypeSuite = {
|
||||
CustomSectionAPI,
|
||||
};
|
||||
export default exportedTypeSuite;
|
||||
10
app/plugin/CustomSectionAPI.ts
Normal file
10
app/plugin/CustomSectionAPI.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* API definitions for CustomSection plugins.
|
||||
*/
|
||||
|
||||
|
||||
import {RenderTarget} from './RenderOptions';
|
||||
|
||||
export interface CustomSectionAPI {
|
||||
createSection(inlineTarget: RenderTarget): Promise<void>;
|
||||
}
|
||||
41
app/plugin/FileParserAPI-ti.ts
Normal file
41
app/plugin/FileParserAPI-ti.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as t from "ts-interface-checker";
|
||||
// tslint:disable:object-literal-key-quotes
|
||||
|
||||
export const EditOptionsAPI = t.iface([], {
|
||||
"getParseOptions": t.func("ParseOptions", t.param("parseOptions", "ParseOptions", true)),
|
||||
});
|
||||
|
||||
export const ParseFileAPI = t.iface([], {
|
||||
"parseFile": t.func("ParseFileResult", t.param("file", "FileSource"), t.param("parseOptions", "ParseOptions", true)),
|
||||
});
|
||||
|
||||
export const ParseOptions = t.iface([], {
|
||||
"NUM_ROWS": t.opt("number"),
|
||||
"SCHEMA": t.opt(t.array("ParseOptionSchema")),
|
||||
});
|
||||
|
||||
export const ParseOptionSchema = t.iface([], {
|
||||
"name": "string",
|
||||
"label": "string",
|
||||
"type": "string",
|
||||
"visible": "boolean",
|
||||
});
|
||||
|
||||
export const FileSource = t.iface([], {
|
||||
"path": "string",
|
||||
"origName": "string",
|
||||
});
|
||||
|
||||
export const ParseFileResult = t.iface(["GristTables"], {
|
||||
"parseOptions": "ParseOptions",
|
||||
});
|
||||
|
||||
const exportedTypeSuite: t.ITypeSuite = {
|
||||
EditOptionsAPI,
|
||||
ParseFileAPI,
|
||||
ParseOptions,
|
||||
ParseOptionSchema,
|
||||
FileSource,
|
||||
ParseFileResult,
|
||||
};
|
||||
export default exportedTypeSuite;
|
||||
52
app/plugin/FileParserAPI.ts
Normal file
52
app/plugin/FileParserAPI.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* API definitions for FileParser plugins.
|
||||
*/
|
||||
|
||||
import {GristTables} from './GristTable';
|
||||
|
||||
export interface EditOptionsAPI {
|
||||
getParseOptions(parseOptions?: ParseOptions): Promise<ParseOptions>;
|
||||
}
|
||||
|
||||
export interface ParseFileAPI {
|
||||
parseFile(file: FileSource, parseOptions?: ParseOptions): Promise<ParseFileResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* ParseOptions contains parse options depending on plugin,
|
||||
* number of rows, which is special option that can be used for any plugin
|
||||
* and schema for generating parse options UI
|
||||
*/
|
||||
export interface ParseOptions {
|
||||
NUM_ROWS?: number;
|
||||
SCHEMA?: ParseOptionSchema[];
|
||||
}
|
||||
|
||||
/**
|
||||
* ParseOptionSchema contains information for generaing parse options UI
|
||||
*/
|
||||
export interface ParseOptionSchema {
|
||||
name: string;
|
||||
label: string;
|
||||
type: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export interface FileSource {
|
||||
/**
|
||||
* The path is often a temporary file, so its name is meaningless. Access to the file depends on
|
||||
* the type of plugin. For instance, for `safePython` plugins file is directly available at
|
||||
* `/importDir/path`.
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* Plugins that want to know the original filename should use origName. Depending on the source
|
||||
* of the data, it may or may not be meaningful.
|
||||
*/
|
||||
origName: string;
|
||||
}
|
||||
|
||||
export interface ParseFileResult extends GristTables {
|
||||
parseOptions: ParseOptions;
|
||||
}
|
||||
34
app/plugin/GristAPI-ti.ts
Normal file
34
app/plugin/GristAPI-ti.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* This module was automatically generated by `ts-interface-builder`
|
||||
*/
|
||||
import * as t from "ts-interface-checker";
|
||||
// tslint:disable:object-literal-key-quotes
|
||||
|
||||
export const ComponentKind = t.union(t.lit("safeBrowser"), t.lit("safePython"), t.lit("unsafeNode"));
|
||||
|
||||
export const GristAPI = t.iface([], {
|
||||
"render": t.func("number", t.param("path", "string"), t.param("target", "RenderTarget"),
|
||||
t.param("options", "RenderOptions", true)),
|
||||
"dispose": t.func("void", t.param("procId", "number")),
|
||||
"subscribe": t.func("void", t.param("tableId", "string")),
|
||||
"unsubscribe": t.func("void", t.param("tableId", "string")),
|
||||
});
|
||||
|
||||
export const GristDocAPI = t.iface([], {
|
||||
"getDocName": t.func("string"),
|
||||
"listTables": t.func(t.array("string")),
|
||||
"fetchTable": t.func("any", t.param("tableId", "string")),
|
||||
"applyUserActions": t.func("any", t.param("actions", t.array(t.array("any")))),
|
||||
});
|
||||
|
||||
export const GristView = t.iface([], {
|
||||
"fetchSelectedTable": t.func("any"),
|
||||
});
|
||||
|
||||
const exportedTypeSuite: t.ITypeSuite = {
|
||||
ComponentKind,
|
||||
GristAPI,
|
||||
GristDocAPI,
|
||||
GristView,
|
||||
};
|
||||
export default exportedTypeSuite;
|
||||
99
app/plugin/GristAPI.ts
Normal file
99
app/plugin/GristAPI.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* This file defines the interface for the grist api exposed to SafeBrowser plugins. Grist supports
|
||||
* various ways to require it to cover various scenarios. If writing the main safeBrowser module
|
||||
* (the one referenced by the components.safeBrowser key of the manifest) use
|
||||
* `self.importScript('grist');`, if writing a view include the script in the html `<script src="grist"></script>`
|
||||
*
|
||||
*
|
||||
* Example usage (let's assume that Grist let's plugin contributes to a Foo API defined as follow ):
|
||||
*
|
||||
* interface Foo {
|
||||
* foo(name: string): Promise<string>;
|
||||
* }
|
||||
*
|
||||
* > main.ts:
|
||||
* class MyFoo {
|
||||
* public foo(name: string): Promise<string> {
|
||||
* return new Promise<string>( async resolve => {
|
||||
* grist.rpc.onMessage( e => {
|
||||
* resolve(e.data + name);
|
||||
* });
|
||||
* grist.ready();
|
||||
* await grist.api.render('view1.html', 'fullscreen');
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
* grist.rpc.registerImpl<Foo>('grist', new MyFoo()); // can add 3rd arg with type information
|
||||
*
|
||||
* > view1.html includes:
|
||||
* grist.api.render('static/view2.html', 'fullscreen').then( view => {
|
||||
* grist.rpc.onMessage(e => grist.rpc.postMessageForward("main.ts", e.data));
|
||||
* });
|
||||
*
|
||||
* > view2.html includes:
|
||||
* grist.rpc.postMessage('view1.html', 'foo ');
|
||||
*
|
||||
*/
|
||||
|
||||
import {RenderOptions, RenderTarget} from './RenderOptions';
|
||||
|
||||
export type ComponentKind = "safeBrowser" | "safePython" | "unsafeNode";
|
||||
|
||||
export const RPC_GRISTAPI_INTERFACE = '_grist_api';
|
||||
|
||||
export interface GristAPI {
|
||||
/**
|
||||
* Render the file at `path` into the `target` location in Grist. `path` must be relative to the
|
||||
* root of the plugin's directory and point to an html that is contained within the plugin's
|
||||
* directory. `target` is a predifined location of the Grist UI, it could be `fullscreen` or
|
||||
* identifier for an inline target. Grist provides inline target identifiers in certain call
|
||||
* plugins. E.g. ImportSourceAPI.getImportSource is given a target identifier to allow rende UI
|
||||
* inline in the import dialog. Returns the procId which can be used to dispose the view.
|
||||
*/
|
||||
render(path: string, target: RenderTarget, options?: RenderOptions): Promise<number>;
|
||||
|
||||
/**
|
||||
* Dispose the process with id procId. If the process was embedded into the UI, removes the
|
||||
* corresponding element from the view.
|
||||
*/
|
||||
dispose(procId: number): Promise<void>;
|
||||
|
||||
// Subscribes to actions for `tableId`. Actions of all subscribed tables are send as rpc's
|
||||
// message.
|
||||
// TODO: document format of messages that can be listened on `rpc.onMessage(...);`
|
||||
subscribe(tableId: string): Promise<void>;
|
||||
|
||||
// Unsubscribe from actions for `tableId`.
|
||||
unsubscribe(tableId: string): Promise<void>;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* GristDocAPI interface is implemented by Grist, and allows getting information from and
|
||||
* interacting with the Grist document to which a plugin is attached.
|
||||
*/
|
||||
export interface GristDocAPI {
|
||||
// Returns the docName that identifies the document.
|
||||
getDocName(): Promise<string>;
|
||||
|
||||
// Returns a sorted list of table IDs.
|
||||
listTables(): Promise<string[]>;
|
||||
|
||||
// Returns a complete table of data in the format {colId: [values]}, including the 'id' column.
|
||||
// Do not modify the returned arrays in-place, especially if used directly (not over RPC).
|
||||
// TODO: return type is Promise{[colId: string]: CellValue[]}> but cannot be specified because
|
||||
// ts-interface-builder does not properly support index-signature.
|
||||
fetchTable(tableId: string): Promise<any>;
|
||||
|
||||
// Applies an array of user actions.
|
||||
// todo: return type should be Promise<ApplyUAResult>, but this requires importing modules from
|
||||
// `app/common` which is not currently supported by the build.
|
||||
applyUserActions(actions: any[][]): Promise<any>;
|
||||
}
|
||||
|
||||
export interface GristView {
|
||||
// Like fetchTable, but gets data for the custom section specifically, if there is any.
|
||||
// TODO: return type is Promise{[colId: string]: CellValue[]}> but cannot be specified because
|
||||
// ts-interface-builder does not properly support index-signature.
|
||||
fetchSelectedTable(): Promise<any>;
|
||||
}
|
||||
24
app/plugin/GristTable-ti.ts
Normal file
24
app/plugin/GristTable-ti.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as t from "ts-interface-checker";
|
||||
// tslint:disable:object-literal-key-quotes
|
||||
|
||||
export const GristTable = t.iface([], {
|
||||
"table_name": t.union("string", "null"),
|
||||
"column_metadata": t.array("GristColumn"),
|
||||
"table_data": t.array(t.array("any")),
|
||||
});
|
||||
|
||||
export const GristTables = t.iface([], {
|
||||
"tables": t.array("GristTable"),
|
||||
});
|
||||
|
||||
export const GristColumn = t.iface([], {
|
||||
"id": "string",
|
||||
"type": "string",
|
||||
});
|
||||
|
||||
const exportedTypeSuite: t.ITypeSuite = {
|
||||
GristTable,
|
||||
GristTables,
|
||||
GristColumn,
|
||||
};
|
||||
export default exportedTypeSuite;
|
||||
38
app/plugin/GristTable.ts
Normal file
38
app/plugin/GristTable.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Common definitions for Grist plugin APIs.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* Metadata and data for a table. This is documenting what is currently returned by the
|
||||
* core plugins. Could be worth reconciling with:
|
||||
* https://phab.getgrist.com/w/grist_data_format/
|
||||
* Capitalization is python-style.
|
||||
*
|
||||
*/
|
||||
export interface GristTable {
|
||||
table_name: string | null; // currently allow names to be null
|
||||
column_metadata: GristColumn[];
|
||||
table_data: any[][];
|
||||
}
|
||||
|
||||
export interface GristTables {
|
||||
tables: GristTable[];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Metadata about a single column.
|
||||
*
|
||||
*/
|
||||
export interface GristColumn {
|
||||
id: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export enum APIType {
|
||||
ImportSourceAPI,
|
||||
ImportProcessorAPI,
|
||||
ParseOptionsAPI,
|
||||
ParseFileAPI,
|
||||
}
|
||||
44
app/plugin/ImportSourceAPI-ti.ts
Normal file
44
app/plugin/ImportSourceAPI-ti.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* This module was automatically generated by `ts-interface-builder`
|
||||
*/
|
||||
import * as t from "ts-interface-checker";
|
||||
// tslint:disable:object-literal-key-quotes
|
||||
|
||||
export const ImportSourceAPI = t.iface([], {
|
||||
"getImportSource": t.func(t.union("ImportSource", "undefined")),
|
||||
});
|
||||
|
||||
export const ImportProcessorAPI = t.iface([], {
|
||||
"processImport": t.func(t.array("GristTable"), t.param("source", "ImportSource")),
|
||||
});
|
||||
|
||||
export const FileContent = t.iface([], {
|
||||
"content": "any",
|
||||
"name": "string",
|
||||
});
|
||||
|
||||
export const FileListItem = t.iface([], {
|
||||
"kind": t.lit("fileList"),
|
||||
"files": t.array("FileContent"),
|
||||
});
|
||||
|
||||
export const URL = t.iface([], {
|
||||
"kind": t.lit("url"),
|
||||
"url": "string",
|
||||
});
|
||||
|
||||
export const ImportSource = t.iface([], {
|
||||
"item": t.union("FileListItem", "URL"),
|
||||
"options": t.opt(t.union("string", "Buffer")),
|
||||
"description": t.opt("string"),
|
||||
});
|
||||
|
||||
const exportedTypeSuite: t.ITypeSuite = {
|
||||
ImportSourceAPI,
|
||||
ImportProcessorAPI,
|
||||
FileContent,
|
||||
FileListItem,
|
||||
URL,
|
||||
ImportSource,
|
||||
};
|
||||
export default exportedTypeSuite;
|
||||
53
app/plugin/ImportSourceAPI.ts
Normal file
53
app/plugin/ImportSourceAPI.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* API definitions for ImportSource plugins.
|
||||
*/
|
||||
|
||||
import { GristTable } from './GristTable';
|
||||
|
||||
export interface ImportSourceAPI {
|
||||
/**
|
||||
* Returns a promise that resolves to an `ImportSource` which is then passed for import to the
|
||||
* import modal dialog. `undefined` interrupts the workflow and prevent the modal from showing up,
|
||||
* but not an empty list of `ImportSourceItem`. Which is a valid import source and is used in
|
||||
* cases where only options are to be sent to an `ImportProcessAPI` implementation.
|
||||
*/
|
||||
getImportSource(): Promise<ImportSource|undefined>;
|
||||
}
|
||||
|
||||
export interface ImportProcessorAPI {
|
||||
processImport(source: ImportSource): Promise<GristTable[]>;
|
||||
}
|
||||
|
||||
export interface FileContent {
|
||||
content: any;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface FileListItem {
|
||||
kind: "fileList";
|
||||
// TODO: there're might be a better way to send file content. In particular for electron where
|
||||
// file will then be send from client to server where it shouldn't be really. An idea could be to
|
||||
// expose something similar to `client/lib/upload.ts` to let plugins create upload entries, and
|
||||
// then send only uploads ids over the rpc.
|
||||
files: FileContent[];
|
||||
}
|
||||
|
||||
export interface URL {
|
||||
kind: "url";
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ImportSource {
|
||||
item: FileListItem | URL;
|
||||
|
||||
/**
|
||||
* The options are only passed within this plugin, nothing else needs to know how they are
|
||||
* serialized. Using JSON.stringify/JSON.parse is a simple approach.
|
||||
*/
|
||||
options?: string|Buffer;
|
||||
|
||||
/**
|
||||
* The short description that shows in the import dialog after source have been selected.
|
||||
*/
|
||||
description?: string;
|
||||
}
|
||||
11
app/plugin/InternalImportSourceAPI-ti.ts
Normal file
11
app/plugin/InternalImportSourceAPI-ti.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import * as t from "ts-interface-checker";
|
||||
// tslint:disable:object-literal-key-quotes
|
||||
|
||||
export const InternalImportSourceAPI = t.iface([], {
|
||||
"getImportSource": t.func(t.union("ImportSource", "undefined"), t.param("inlineTarget", "RenderTarget")),
|
||||
});
|
||||
|
||||
const exportedTypeSuite: t.ITypeSuite = {
|
||||
InternalImportSourceAPI,
|
||||
};
|
||||
export default exportedTypeSuite;
|
||||
24
app/plugin/InternalImportSourceAPI.ts
Normal file
24
app/plugin/InternalImportSourceAPI.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { RenderTarget } from './RenderOptions';
|
||||
|
||||
import { ImportSource } from './ImportSourceAPI';
|
||||
|
||||
export * from './ImportSourceAPI';
|
||||
|
||||
/**
|
||||
* This internal interface is implemented by grist-plugin-api.ts to support
|
||||
* `grist.addImporter(...)`. This is this interface that grist stubs to calls
|
||||
* `ImportSourceAPI`. However, some of the complexity (ie: rendering targets) is hidden from the
|
||||
* plugin author which implements directly the simpler `ImportSourceAPI`.
|
||||
*
|
||||
* Reason for this interface is because we want to have the `inlineTarget` parameter but we don't
|
||||
* want plugin author to have it.
|
||||
*/
|
||||
export interface InternalImportSourceAPI {
|
||||
/**
|
||||
* The `inlineTarget` argument which will be passed to the implementation of this method, can be
|
||||
* used as follow `grist.api.render('index.html', inlineTarget)` to embbed `index.html` in the
|
||||
* import panel. Or it can be ignored and use `'fullscreen'` in-place. It is used in
|
||||
* `grist.addImporter(...)` according to the value of the `mode` argument.
|
||||
*/
|
||||
getImportSource(inlineTarget: RenderTarget): Promise<ImportSource|undefined>;
|
||||
}
|
||||
60
app/plugin/PluginManifest-ti.ts
Normal file
60
app/plugin/PluginManifest-ti.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* This module was automatically generated by `ts-interface-builder`
|
||||
*/
|
||||
import * as t from "ts-interface-checker";
|
||||
// tslint:disable:object-literal-key-quotes
|
||||
|
||||
export const PublishedPlugin = t.iface(["BarePlugin"], {
|
||||
"name": "string",
|
||||
"version": "string",
|
||||
});
|
||||
|
||||
export const BarePlugin = t.iface([], {
|
||||
"components": t.iface([], {
|
||||
"safeBrowser": t.opt("string"),
|
||||
"safePython": t.opt("string"),
|
||||
"unsafeNode": t.opt("string"),
|
||||
"deactivate": t.opt(t.iface([], {
|
||||
"inactivitySec": t.opt("number"),
|
||||
})),
|
||||
}),
|
||||
"contributions": t.iface([], {
|
||||
"importSources": t.opt(t.array("ImportSource")),
|
||||
"fileParsers": t.opt(t.array("FileParser")),
|
||||
"customSections": t.opt(t.array("CustomSection")),
|
||||
}),
|
||||
"experimental": t.opt("boolean"),
|
||||
});
|
||||
|
||||
export const ImportSource = t.iface([], {
|
||||
"label": "string",
|
||||
"importSource": "Implementation",
|
||||
"importProcessor": t.opt("Implementation"),
|
||||
});
|
||||
|
||||
export const FileParser = t.iface([], {
|
||||
"fileExtensions": t.array("string"),
|
||||
"editOptions": t.opt("Implementation"),
|
||||
"parseFile": "Implementation",
|
||||
});
|
||||
|
||||
export const CustomSection = t.iface([], {
|
||||
"path": "string",
|
||||
"name": "string",
|
||||
});
|
||||
|
||||
export const Implementation = t.iface([], {
|
||||
"component": t.union(t.lit("safeBrowser"), t.lit("safePython"), t.lit("unsafeNode")),
|
||||
"name": "string",
|
||||
"path": t.opt("string"),
|
||||
});
|
||||
|
||||
const exportedTypeSuite: t.ITypeSuite = {
|
||||
PublishedPlugin,
|
||||
BarePlugin,
|
||||
ImportSource,
|
||||
FileParser,
|
||||
CustomSection,
|
||||
Implementation,
|
||||
};
|
||||
export default exportedTypeSuite;
|
||||
219
app/plugin/PluginManifest.ts
Normal file
219
app/plugin/PluginManifest.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* This file defines the interface for a plugin manifest.
|
||||
*
|
||||
* Note that it is possible to validate a manifest against a TypeScript interface as follows:
|
||||
* (1) Convert the interface to a JSON schema at build time using
|
||||
* https://www.npmjs.com/package/typescript-json-schema:
|
||||
* bin/typescript-json-schema --required --noExtraProps PluginManifest.ts PublishedPlugin
|
||||
* (2) Use a JSON schema validator like https://www.npmjs.com/package/ajv to validate manifests
|
||||
* read at run-time and produce informative errors automatically.
|
||||
*
|
||||
* TODO [Proposal]: To save an ImportSource for reuse, we would save:
|
||||
* {
|
||||
* pluginId: string;
|
||||
* importSource: ImportSource;
|
||||
* importProcessor?: Implementation;
|
||||
* parseOptions?: ParseOptions; // If importProcessor is omitted and fileParser is used.
|
||||
* }
|
||||
* This should suffice for database re-imports, as well as for re-imports from a URL, or from a
|
||||
* saved path in the filesystem (which can be a builtIn plugin available for Electron version).
|
||||
*/
|
||||
|
||||
/**
|
||||
* PublishedPlugin is a BarePlugin with additional attributes to identify and describe a plugin
|
||||
* for publishing.
|
||||
*/
|
||||
export interface PublishedPlugin extends BarePlugin {
|
||||
name: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* BarePlugin defines the functionality of a plugin. It is the only part required for a plugin to
|
||||
* function, and is implemented by built-in plugins, published plugins, and private plugins (such
|
||||
* as those being developed).
|
||||
*/
|
||||
export interface BarePlugin {
|
||||
/**
|
||||
* Components describe how the plugin runs. A plugin may provide UI and behavior that runs in
|
||||
* the browser, Python code that runs in a secure sandbox, and arbitrary code that runs in Node.
|
||||
*/
|
||||
components: {
|
||||
/**
|
||||
* Relative path to the directory whose content will be served to the browser. Required for
|
||||
* those plugins that need to render their own HTML or run in-browser Javascript. This
|
||||
* directory should contain all html files referenced in the manifest.
|
||||
*
|
||||
* It is "safe" in that Grist offers protections that allow such plugins to be marked "safe".
|
||||
*/
|
||||
safeBrowser?: string;
|
||||
|
||||
/**
|
||||
* Relative path to a file with Python code that will be run in a python sandbox. This
|
||||
* file is started on plugin activation, and should register any implemented APIs.
|
||||
* Required for plugins that do Python processing.
|
||||
*
|
||||
* It is "safe" in that Grist offers protections that allow such plugins to be marked "safe".
|
||||
*/
|
||||
safePython?: string;
|
||||
|
||||
/**
|
||||
* Relative path to a file containing Javascript code that will be executed with Node.js.
|
||||
* The code is called on plugin activation, and should register any implemented APIs
|
||||
* once we've figured out how that should happen (TODO). Required for plugins that need
|
||||
* to do any "unsafe" work, such as accessing the local filesystem or starting helper
|
||||
* programs.
|
||||
*
|
||||
* It is "unsafe" in that it can do too much, and Grist marks such plugins as "unsafe".
|
||||
*
|
||||
* An unsafeNode component opens as separate process to run plugin node code, with the
|
||||
* NODE_PATH set to the plugin directory. The node code can execute arbitrary actions -
|
||||
* there is no sandboxing.
|
||||
*
|
||||
* The node child may communicate with the server via standard ChildProcess ipc
|
||||
* (`process.send`, `process.on('message', ...)`). The child is expected to
|
||||
* `process.send` a message to the server once it is listening to the `message`
|
||||
* event. That message is expected to contain a `ready` field set to `true`. All
|
||||
* other communication should follow the protocol implemented by the Rpc module.
|
||||
* TODO: provide plugin authors with documentation + library to use that implements
|
||||
* these requirements.
|
||||
*
|
||||
*/
|
||||
unsafeNode?: string;
|
||||
|
||||
/**
|
||||
* Options for when to deactivate the plugin, i.e. when to stop any plugin processes. (Note
|
||||
* that we may in the future also add options for when to activate the plugin, which is for
|
||||
* now automatic and not configurable.)
|
||||
*/
|
||||
deactivate?: {
|
||||
// Deactivate after this many seconds of inactivity. Defaults to 300 (5 minutes) if omitted.
|
||||
inactivitySec?: number;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Contributions describe what new functionality the plugin contributes to the Grist
|
||||
* application. See documentation for individual contribution types for details. Any plugin may
|
||||
* provide multiple contributions. It is common to provide just one, in which case include a
|
||||
* single property with a single-element array.
|
||||
*/
|
||||
contributions: {
|
||||
importSources?: ImportSource[];
|
||||
fileParsers?: FileParser[];
|
||||
customSections?: CustomSection[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Experimental plugins run only if the environment variable GRIST_EXPERIMENTAL_PLUGINS is
|
||||
* set. Otherwise they are ignored. This is useful for plugins that needs a bit of experimentation
|
||||
* before being pushed to production (ie: production does not have GRIST_EXPERIMENTAL_PLUGINS set
|
||||
* but staging does). Keep in mind that developers need to set this environment if they want to
|
||||
* run them locally.
|
||||
*/
|
||||
experimental?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* An ImportSource plugin creates a new source of imports, such as an external API, a file-sharing
|
||||
* service, or a new type of database. It adds a new item for the user to select when importing.
|
||||
*/
|
||||
export interface ImportSource {
|
||||
/**
|
||||
* Label shows up as a new item for the user to select when starting an import.
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* Implementation of ImportSourceAPI. Supports safeBrowser component, which allows you to create
|
||||
* custom UI to show to the user. Or describe UI using a .json or .yml config file and use
|
||||
* {component: "builtIn", name: "importSourceConfig", path: "your-config"}.
|
||||
*/
|
||||
importSource: Implementation;
|
||||
|
||||
/**
|
||||
* Implementation of ImportProcessorAPI. It receives the output of importSource, and produces
|
||||
* Grist data. If omitted, uses the default ImportProcessor, which is equivalent to
|
||||
* {component: "builtIn", name: "fileParser"}.
|
||||
*
|
||||
* The default ImportProcessor handles received ImportSourceItems as follows:
|
||||
* (1) items of type "file" are saved to temp files.
|
||||
* (2) items of type "url" are downloaded to temp files.
|
||||
* (3) calls ParseFileAPI.parseFile() with all temp files, to produce Grist tables
|
||||
* (4) returns those Grist tables along with all items of type "table".
|
||||
* Note that the default ImportParser ignores ImportSource items of type "custom".
|
||||
*/
|
||||
importProcessor?: Implementation;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A FileParser plugin adds support to parse a new type of file data, such as "csv", "yml", or
|
||||
* "ods". It then enables importing the new type of file via upload or from any other ImportSource
|
||||
* that produces Files or URLs.
|
||||
*/
|
||||
export interface FileParser {
|
||||
/**
|
||||
* File extensions for which this FileParser should be considered, e.g. "csv", "yml". You may
|
||||
* use "" for files with no extensions, and "*" to match any extension.
|
||||
*/
|
||||
fileExtensions: string[];
|
||||
|
||||
/**
|
||||
* Implementation of EditOptionsAPI. Supports safeBrowser component, which allows you to create
|
||||
* custom UI to show to the user. Or describe UI using a .json or .yml config file and use
|
||||
* {component: "builtIn", name: "parseOptionsConfig", path: "your-config"}.
|
||||
*
|
||||
* If omitted, the user will be shown no parse options.
|
||||
*/
|
||||
editOptions?: Implementation;
|
||||
|
||||
/**
|
||||
* Implementation of ParseFileAPI, which converts Files to Grist data using parse options.
|
||||
*/
|
||||
parseFile: Implementation;
|
||||
}
|
||||
|
||||
/**
|
||||
* A CustomSection plugin adds support to add new types of section to Grist, such as a calendar,
|
||||
* maps, data visualizations.
|
||||
*/
|
||||
export interface CustomSection {
|
||||
/**
|
||||
* Path to an html file.
|
||||
*/
|
||||
path: string;
|
||||
/**
|
||||
* The name should uniquely identify the section in the plugin.
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Plugin supplies one or more Implementation of some APIs. Components register implementation
|
||||
* using a call such as:
|
||||
* grist.register(SomeAPI, 'myName', impl).
|
||||
* The manifest documentation describes which API must be implemented at any particular point, and
|
||||
* it is the plugin's responsibility to register an implementation of the correct API and refer to
|
||||
* it by Implementation.name.
|
||||
*/
|
||||
export interface Implementation {
|
||||
/**
|
||||
* Which component of the plugin provides this implementation.
|
||||
*/
|
||||
component: "safeBrowser" | "safePython" | "unsafeNode";
|
||||
|
||||
/**
|
||||
* The name of the implementation registered by the chosen component. The same component can
|
||||
* register any number of APIs at any names.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Path is used by safeBrowser component for which page to load. Defaults to 'index.html'.
|
||||
* It is also used by certain builtIn implementation, e.g. if name is 'parse-options-config',
|
||||
* path is the path to JSON or YAML file containing the configuration.
|
||||
*/
|
||||
path?: string;
|
||||
}
|
||||
14
app/plugin/RenderOptions-ti.ts
Normal file
14
app/plugin/RenderOptions-ti.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import * as t from "ts-interface-checker";
|
||||
// tslint:disable:object-literal-key-quotes
|
||||
|
||||
export const RenderTarget = t.union(t.lit("fullscreen"), "number");
|
||||
|
||||
export const RenderOptions = t.iface([], {
|
||||
"height": t.opt("string"),
|
||||
});
|
||||
|
||||
const exportedTypeSuite: t.ITypeSuite = {
|
||||
RenderTarget,
|
||||
RenderOptions,
|
||||
};
|
||||
export default exportedTypeSuite;
|
||||
11
app/plugin/RenderOptions.ts
Normal file
11
app/plugin/RenderOptions.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Where to append the content that a plugin renders.
|
||||
*/
|
||||
export type RenderTarget = "fullscreen" | number;
|
||||
|
||||
/**
|
||||
* Options for the `grist.render` function.
|
||||
*/
|
||||
export interface RenderOptions {
|
||||
height?: string;
|
||||
}
|
||||
15
app/plugin/StorageAPI-ti.ts
Normal file
15
app/plugin/StorageAPI-ti.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as t from "ts-interface-checker";
|
||||
// tslint:disable:object-literal-key-quotes
|
||||
|
||||
export const Storage = t.iface([], {
|
||||
"getItem": t.func("any", t.param("key", "string")),
|
||||
"hasItem": t.func("boolean", t.param("key", "string")),
|
||||
"setItem": t.func("void", t.param("key", "string"), t.param("value", "any")),
|
||||
"removeItem": t.func("void", t.param("key", "string")),
|
||||
"clear": t.func("void"),
|
||||
});
|
||||
|
||||
const exportedTypeSuite: t.ITypeSuite = {
|
||||
Storage,
|
||||
};
|
||||
export default exportedTypeSuite;
|
||||
8
app/plugin/StorageAPI.ts
Normal file
8
app/plugin/StorageAPI.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// subset of WebStorage API
|
||||
export interface Storage {
|
||||
getItem(key: string): any;
|
||||
hasItem(key: string): boolean;
|
||||
setItem(key: string, value: any): void;
|
||||
removeItem(key: string): void;
|
||||
clear(): void;
|
||||
}
|
||||
50
app/plugin/TypeCheckers.ts
Normal file
50
app/plugin/TypeCheckers.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import {createCheckers, ICheckerSuite} from 'ts-interface-checker';
|
||||
import CustomSectionAPITI from './CustomSectionAPI-ti';
|
||||
import FileParserAPITI from './FileParserAPI-ti';
|
||||
import GristAPITI from './GristAPI-ti';
|
||||
import GristTableTI from './GristTable-ti';
|
||||
import ImportSourceAPITI from './ImportSourceAPI-ti';
|
||||
import InternalImportSourceAPITI from './InternalImportSourceAPI-ti';
|
||||
import RenderOptionsTI from './RenderOptions-ti';
|
||||
import StorageAPITI from './StorageAPI-ti';
|
||||
|
||||
/**
|
||||
* The ts-interface-checker type suites are all exported with the "TI" suffix.
|
||||
*/
|
||||
export {
|
||||
CustomSectionAPITI, FileParserAPITI, GristAPITI, GristTableTI, ImportSourceAPITI,
|
||||
InternalImportSourceAPITI, RenderOptionsTI, StorageAPITI};
|
||||
|
||||
const allTypes = [
|
||||
CustomSectionAPITI, FileParserAPITI, GristAPITI, GristTableTI, ImportSourceAPITI,
|
||||
InternalImportSourceAPITI, RenderOptionsTI, StorageAPITI];
|
||||
|
||||
function checkDuplicates(types: Array<{[key: string]: object}>) {
|
||||
const seen = new Set<string>();
|
||||
for (const t of types) {
|
||||
for (const key of Object.keys(t)) {
|
||||
if (seen.has(key)) { throw new Error(`TypeCheckers: Duplicate type name ${key}`); }
|
||||
seen.add(key);
|
||||
// Uncomment the line below to generate updated list of included types.
|
||||
// console.log(`'${key}' |`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkDuplicates(allTypes);
|
||||
|
||||
/**
|
||||
* We also create and export a global checker object that includes all of the types above.
|
||||
*/
|
||||
export const checkers = createCheckers(...allTypes) as (
|
||||
// The following Pick typecast ensures that Typescript can only use correct properties of the
|
||||
// checkers object (e.g. checkers.GristAPI will compile, but checkers.GristApi will not).
|
||||
// TODO: The restrictive type of ICheckerSuite should be generated automatically. (Currently
|
||||
// generated by commenting out console.log() in checkDuplicates() above.)
|
||||
Pick<ICheckerSuite,
|
||||
'CustomSectionAPI' | 'EditOptionsAPI' | 'ParseFileAPI' | 'ParseOptions' | 'ParseOptionSchema' |
|
||||
'FileSource' | 'ParseFileResult' | 'ComponentKind' | 'GristAPI' | 'GristDocAPI' | 'GristTable' |
|
||||
'GristTables' | 'GristColumn' | 'GristView' | 'ImportSourceAPI' | 'ImportProcessorAPI' | 'FileContent' |
|
||||
'FileListItem' | 'URL' | 'ImportSource' | 'InternalImportSourceAPI' | 'RenderTarget' |
|
||||
'RenderOptions' | 'Storage'
|
||||
>);
|
||||
135
app/plugin/grist-plugin-api.ts
Normal file
135
app/plugin/grist-plugin-api.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
// Provide a way to acess grist for iframe, web worker (which runs the main safeBrowser script) and
|
||||
// unsafeNode. WebView should work the same way as iframe, grist is exposed just the same way and
|
||||
// necessary api is exposed using preload script. Here we bootstrap from channel capabilities to key
|
||||
// parts of the grist API.
|
||||
|
||||
// For iframe (and webview):
|
||||
// user will add '<script src="/grist-api.js"></script>' and get a window.grist
|
||||
|
||||
// For web worker:
|
||||
// use will add `self.importScripts('/grist-api.js');`
|
||||
|
||||
// For node, user will do something like:
|
||||
// const {grist} = require('grist-api');
|
||||
// grist.registerFunction();
|
||||
// In TypeScript:
|
||||
// import {grist} from 'grist-api';
|
||||
// grist.registerFunction();
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
import { GristAPI, GristDocAPI, GristView, RPC_GRISTAPI_INTERFACE } from './GristAPI';
|
||||
import { ImportSource, ImportSourceAPI, InternalImportSourceAPI } from './InternalImportSourceAPI';
|
||||
import { RenderOptions, RenderTarget } from './RenderOptions';
|
||||
import { checkers } from './TypeCheckers';
|
||||
|
||||
export * from './TypeCheckers';
|
||||
export * from './FileParserAPI';
|
||||
export * from './GristAPI';
|
||||
export * from './GristTable';
|
||||
export * from './ImportSourceAPI';
|
||||
export * from './StorageAPI';
|
||||
export * from './RenderOptions';
|
||||
|
||||
import {IRpcLogger, Rpc} from 'grain-rpc';
|
||||
|
||||
export const rpc: Rpc = new Rpc({logger: createRpcLogger()});
|
||||
|
||||
export const api = rpc.getStub<GristAPI>(RPC_GRISTAPI_INTERFACE, checkers.GristAPI);
|
||||
export const coreDocApi = rpc.getStub<GristDocAPI>('GristDocAPI@grist', checkers.GristDocAPI);
|
||||
export const viewApi = rpc.getStub<GristView>('GristView', checkers.GristView);
|
||||
export const docApi = {
|
||||
...coreDocApi,
|
||||
...viewApi,
|
||||
};
|
||||
|
||||
export const on = rpc.on.bind(rpc);
|
||||
|
||||
/**
|
||||
* Calling `addImporter(...)` adds a safeBrowser importer. It is a short-hand for forwarding calls
|
||||
* to an `ImportSourceAPI` implementation registered in the file at `path`. It takes care of
|
||||
* creating the stub, registering an implementation that renders the file, forward the call and
|
||||
* dispose the view properly. If `mode` is `'inline'` embeds the view in the import modal, ohterwise
|
||||
* renders fullscreen.
|
||||
*
|
||||
* Notes: it assumes that file at `path` registers an `ImportSourceAPI` implementation under
|
||||
* `name`. Calling `addImporter(...)` from another component than a `safeBrowser` component is not
|
||||
* currently supported.
|
||||
*
|
||||
*/
|
||||
export async function addImporter(name: string, path: string, mode: 'fullscreen' | 'inline', options?: RenderOptions) {
|
||||
// checker is omitterd for implementation because call was alredy checked by grist.
|
||||
rpc.registerImpl<InternalImportSourceAPI>(name, {
|
||||
async getImportSource(target: RenderTarget): Promise<ImportSource|undefined> {
|
||||
const procId = await api.render(path, mode === 'inline' ? target : 'fullscreen', options);
|
||||
try {
|
||||
// stubName for the interface `name` at forward destination `path`
|
||||
const stubName = `${name}@${path}`;
|
||||
// checker is omitted in stub because call will be checked just after in grist.
|
||||
return await rpc.getStub<ImportSourceAPI>(stubName).getImportSource();
|
||||
} finally {
|
||||
await api.dispose(procId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare that a component is prepared to receive messages from the outside world.
|
||||
* Grist will not attempt to communicate with it until this method is called.
|
||||
*/
|
||||
export function ready(): void {
|
||||
rpc.processIncoming();
|
||||
rpc.sendReadyMessage();
|
||||
}
|
||||
|
||||
function getPluginPath(location: Location) {
|
||||
return location.pathname.replace(/^\/plugins\//, '');
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// Window or iframe.
|
||||
const preloadWindow: any = window;
|
||||
if (preloadWindow.isRunningUnderElectron) {
|
||||
rpc.setSendMessage(msg => preloadWindow.sendToHost(msg));
|
||||
preloadWindow.onGristMessage((data: any) => rpc.receiveMessage(data));
|
||||
} else {
|
||||
rpc.setSendMessage(msg => window.parent.postMessage(msg, "*"));
|
||||
window.onmessage = (e: MessageEvent) => rpc.receiveMessage(e.data);
|
||||
}
|
||||
} else if (typeof process === 'undefined') {
|
||||
// Web worker. We can't really bring in the types for WebWorker (available with --lib flag)
|
||||
// without conflicting with a regular window, so use just use `self as any` here.
|
||||
self.onmessage = (e: MessageEvent) => rpc.receiveMessage(e.data);
|
||||
rpc.setSendMessage((mssg: any) => (self as any).postMessage(mssg));
|
||||
} else if (typeof process.send !== 'undefined') {
|
||||
// Forked ChildProcess of node or electron.
|
||||
// sendMessage callback returns void 0 because rpc process.send returns a boolean and rpc
|
||||
// expecting void|Promise interprets truthy values as Promise which cause failure.
|
||||
rpc.setSendMessage((data) => { process.send!(data); });
|
||||
process.on('message', (data: any) => rpc.receiveMessage(data));
|
||||
process.on('disconnect', () => { process.exit(0); });
|
||||
} else {
|
||||
// Not a recognized environment, perhaps plain nodejs run independently of Grist, or tests
|
||||
// running under mocha. For now, we only provide a disfunctional implementation. It allows
|
||||
// plugins to call methods like registerFunction() without failing, so that plugin code may be
|
||||
// imported, but the methods don't do anything useful.
|
||||
rpc.setSendMessage((data) => {return; });
|
||||
}
|
||||
|
||||
function createRpcLogger(): IRpcLogger {
|
||||
let prefix: string;
|
||||
if (typeof window !== 'undefined') {
|
||||
prefix = `PLUGIN VIEW ${getPluginPath(window.location)}:`;
|
||||
} else if (typeof process === 'undefined') {
|
||||
prefix = `PLUGIN VIEW ${getPluginPath(self.location)}:`;
|
||||
} else if (typeof process.send !== 'undefined') {
|
||||
prefix = `PLUGIN NODE ${process.env.GRIST_PLUGIN_PATH || "<unset-plugin-id>"}:`;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
info(msg: string) { console.log("%s %s", prefix, msg); },
|
||||
warn(msg: string) { console.warn("%s %s", prefix, msg); },
|
||||
};
|
||||
}
|
||||
3
app/plugin/tsconfig.json
Normal file
3
app/plugin/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../buildtools/tsconfig-base.json",
|
||||
}
|
||||
Reference in New Issue
Block a user