(core) Configure more comprehensive eslint rules for Typescript

Summary:
- Update rules to be more like we've had with tslint
- Switch tsserver plugin to eslint (tsserver makes for a much faster way to lint in editors)
- Apply suggested auto-fixes
- Fix all lint errors and warnings in core/, app/, test/

Test Plan: Some behavior may change subtly (e.g. added missing awaits), relying on existing tests to catch problems.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2785
This commit is contained in:
Dmitry S 2021-04-26 17:54:09 -04:00
parent 91fdef58ac
commit 526b0ad33e
56 changed files with 147 additions and 125 deletions

View File

@ -241,9 +241,9 @@ AceEditor.prototype._getContentHeight = function() {
let _RangeConstructor = null; //singleton, load it lazily let _RangeConstructor = null; //singleton, load it lazily
AceEditor.makeRange = function(a,b,c,d) { AceEditor.makeRange = function(a, b, c, d) {
_RangeConstructor = _RangeConstructor || ace.acequire('ace/range').Range; _RangeConstructor = _RangeConstructor || ace.acequire('ace/range').Range;
return new _RangeConstructor(a,b,c,d); return new _RangeConstructor(a, b, c, d);
}; };
module.exports = AceEditor; module.exports = AceEditor;

View File

@ -262,7 +262,7 @@ export class ActionLog extends dispose.Disposable implements IDomComponent {
if (this._loaded || !this._gristDoc) { return; } if (this._loaded || !this._gristDoc) { return; }
this._loading(true); this._loading(true);
// Returned actions are ordered with earliest actions first. // Returned actions are ordered with earliest actions first.
const result = await this._gristDoc!.docComm.getActionSummaries(); const result = await this._gristDoc.docComm.getActionSummaries();
this._loading(false); this._loading(false);
this._loaded = true; this._loaded = true;
// Add the actions to our action log. // Add the actions to our action log.

View File

@ -15,7 +15,7 @@ import * as ko from 'knockout';
import noop = require('lodash/noop'); import noop = require('lodash/noop');
// To simplify diff (avoid rearranging methods to satisfy private/public order). // To simplify diff (avoid rearranging methods to satisfy private/public order).
// tslint:disable:member-ordering /* eslint-disable @typescript-eslint/member-ordering */
type AceEditor = any; type AceEditor = any;
@ -130,7 +130,7 @@ export class ColumnTransform extends Disposable {
this.transformColumn = this.field.column(); this.transformColumn = this.field.column();
this.transformColumn.origColRef(this.origColumn.getRowId()); this.transformColumn.origColRef(this.origColumn.getRowId());
this._setTransforming(true); this._setTransforming(true);
return await this.postAddTransformColumn(); return this.postAddTransformColumn();
} finally { } finally {
this.isCallPending(false); this.isCallPending(false);
} }
@ -167,7 +167,7 @@ export class ColumnTransform extends Disposable {
/** /**
* A derived class can override to do some processing after this.transformColumn has been set. * A derived class can override to do some processing after this.transformColumn has been set.
*/ */
protected postAddTransformColumn() { protected postAddTransformColumn(): void {
// Nothing in base class. // Nothing in base class.
} }
@ -207,7 +207,7 @@ export class ColumnTransform extends Disposable {
} finally { } finally {
// Wait until the change completed to set column back, to avoid value flickering. // Wait until the change completed to set column back, to avoid value flickering.
field.colRef(origRef); field.colRef(origRef);
tableData.sendTableAction(['RemoveColumn', transformColId]); void tableData.sendTableAction(['RemoveColumn', transformColId]);
this.dispose(); this.dispose();
} }
} }

View File

@ -350,11 +350,11 @@ GridView.prototype.fillSelectionDown = function() {
}).filter(colId => colId); }).filter(colId => colId);
var colInfo = _.object(colIds, colIds.map(colId => { var colInfo = _.object(colIds, colIds.map(colId => {
var val = this.tableModel.tableData.getValue(rowIds[0],colId); var val = this.tableModel.tableData.getValue(rowIds[0], colId);
return rowIds.map(() => val); return rowIds.map(() => val);
})); }));
this.tableModel.sendTableAction(["BulkUpdateRecord",rowIds,colInfo]); this.tableModel.sendTableAction(["BulkUpdateRecord", rowIds, colInfo]);
}; };

View File

@ -153,7 +153,7 @@ export class Importer extends Disposable {
} else if (item.kind === "url") { } else if (item.kind === "url") {
uploadResult = await fetchURL(this._docComm, item.url); uploadResult = await fetchURL(this._docComm, item.url);
} else { } else {
throw new Error(`Import source of kind ${item!.kind} are not yet supported!`); throw new Error(`Import source of kind ${(item as any).kind} are not yet supported!`);
} }
} }
} }
@ -391,7 +391,7 @@ export class Importer extends Disposable {
} }
function getSourceDescription(sourceInfo: SourceInfo, upload: UploadResult) { function getSourceDescription(sourceInfo: SourceInfo, upload: UploadResult) {
const origName = upload!.files[sourceInfo.uploadFileIndex].origName; const origName = upload.files[sourceInfo.uploadFileIndex].origName;
return sourceInfo.origTableName ? origName + ' - ' + sourceInfo.origTableName : origName; return sourceInfo.origTableName ? origName + ' - ' + sourceInfo.origTableName : origName;
} }

View File

@ -25,8 +25,8 @@ export function makeSearchToolbarGroup(gristDoc: GristDoc) {
// Active normally. // Active normally.
const commandGroup = createGroup({ const commandGroup = createGroup({
find: () => { input.focus(); }, find: () => { input.focus(); },
findNext: () => { searcher.findNext(); }, // tslint:disable-line:no-floating-promises TODO findNext: () => { searcher.findNext(); }, // eslint-disable-line @typescript-eslint/no-floating-promises
findPrev: () => { searcher.findPrev(); }, // tslint:disable-line:no-floating-promises TODO findPrev: () => { searcher.findPrev(); }, // eslint-disable-line @typescript-eslint/no-floating-promises
}, null, true); }, null, true);
// Return an array of one item (for a toolbar group of a single item). The item is an array of // Return an array of one item (for a toolbar group of a single item). The item is an array of
@ -49,7 +49,7 @@ export function makeSearchToolbarGroup(gristDoc: GristDoc) {
// the searchbox is created so early that the actions like accept/cancel get overridden). // the searchbox is created so early that the actions like accept/cancel get overridden).
dom.on('keydown', (e: KeyboardEvent) => { dom.on('keydown', (e: KeyboardEvent) => {
switch (e.keyCode) { switch (e.keyCode) {
case 13: searcher.findNext(); break; // tslint:disable-line:no-floating-promises TODO case 13: searcher.findNext(); break; // eslint-disable-line @typescript-eslint/no-floating-promises
case 27: input.blur(); break; case 27: input.blur(); break;
} }
}) })

View File

@ -264,7 +264,7 @@ CellSelector.prototype.rowCount = function() {
return this.rowUpper() - this.rowLower() + 1; return this.rowUpper() - this.rowLower() + 1;
}; };
CellSelector.prototype.selectArea = function(rowStartIdx,colStartIdx,rowEndIdx,colEndIdx) { CellSelector.prototype.selectArea = function(rowStartIdx, colStartIdx, rowEndIdx, colEndIdx) {
this.row.start(rowStartIdx); this.row.start(rowStartIdx);
this.col.start(colStartIdx); this.col.start(colStartIdx);
this.row.end(rowEndIdx); this.row.end(rowEndIdx);

View File

@ -22,7 +22,7 @@ import isEmpty = require('lodash/isEmpty');
import pickBy = require('lodash/pickBy'); import pickBy = require('lodash/pickBy');
// To simplify diff (avoid rearranging methods to satisfy private/public order). // To simplify diff (avoid rearranging methods to satisfy private/public order).
// tslint:disable:member-ordering /* eslint-disable @typescript-eslint/member-ordering */
/** /**
* Creates an instance of TypeTransform for a single field. Extends ColumnTransform. * Creates an instance of TypeTransform for a single field. Extends ColumnTransform.

View File

@ -58,7 +58,7 @@ function ViewConfigTab(options) {
}) || self.viewSectionData.at(0); }) || self.viewSectionData.at(0);
})); }));
this.isDetail = this.autoDispose(ko.computed(function() { this.isDetail = this.autoDispose(ko.computed(function() {
return ['detail','single'].includes(this.viewModel.activeSection().parentKey()); return ['detail', 'single'].includes(this.viewModel.activeSection().parentKey());
}, this)); }, this));
this.isChart = this.autoDispose(ko.computed(function() { this.isChart = this.autoDispose(ko.computed(function() {
return this.viewModel.activeSection().parentKey() === 'chart';}, this)); return this.viewModel.activeSection().parentKey() === 'chart';}, this));

View File

@ -35,7 +35,6 @@ declare module "app/client/components/BaseView" {
import {Cursor, CursorPos} from 'app/client/components/Cursor'; import {Cursor, CursorPos} from 'app/client/components/Cursor';
import {GristDoc} from 'app/client/components/GristDoc'; import {GristDoc} from 'app/client/components/GristDoc';
import {Disposable} from 'app/client/lib/dispose'; import {Disposable} from 'app/client/lib/dispose';
import {KoArray} from "app/client/lib/koArray";
import * as BaseRowModel from "app/client/models/BaseRowModel"; import * as BaseRowModel from "app/client/models/BaseRowModel";
import {DataRowModel} from 'app/client/models/DataRowModel'; import {DataRowModel} from 'app/client/models/DataRowModel';
import {LazyArrayModel} from "app/client/models/DataTableModel"; import {LazyArrayModel} from "app/client/models/DataTableModel";
@ -72,10 +71,8 @@ declare module "app/client/components/BaseView" {
} }
declare module "app/client/components/RefSelect" { declare module "app/client/components/RefSelect" {
import {GristDoc, TabContent} from 'app/client/components/GristDoc';
import {Disposable} from 'app/client/lib/dispose'; import {Disposable} from 'app/client/lib/dispose';
import {ColumnRec} from "app/client/models/DocModel"; import {ColumnRec} from "app/client/models/DocModel";
import {DomArg} from 'grainjs';
import {DocModel} from "app/client/models/DocModel"; import {DocModel} from "app/client/models/DocModel";
import {FieldBuilder} from "app/client/widgets/FieldBuilder"; import {FieldBuilder} from "app/client/widgets/FieldBuilder";
@ -161,11 +158,11 @@ declare module "app/client/models/BaseRowModel" {
class BaseRowModel extends Disposable { class BaseRowModel extends Disposable {
public id: ko.Computed<number>; public id: ko.Computed<number>;
public _index: ko.Observable<number|null>; public _index: ko.Observable<number|null>;
public getRowId(): number;
public updateColValues(colValues: ColValues): Promise<void>;
public _table: TableModel; public _table: TableModel;
protected _rowId: number | 'new' | null; protected _rowId: number | 'new' | null;
protected _fields: string[]; protected _fields: string[];
public getRowId(): number;
public updateColValues(colValues: ColValues): Promise<void>;
} }
export = BaseRowModel; export = BaseRowModel;
} }
@ -286,10 +283,9 @@ declare module "app/client/models/DataTableModel" {
import * as BaseRowModel from "app/client/models/BaseRowModel"; import * as BaseRowModel from "app/client/models/BaseRowModel";
import {DocModel, TableRec} from "app/client/models/DocModel"; import {DocModel, TableRec} from "app/client/models/DocModel";
import {TableQuerySets} from 'app/client/models/QuerySet'; import {TableQuerySets} from 'app/client/models/QuerySet';
import {RowSource, SortedRowSet} from "app/client/models/rowset"; import {SortedRowSet} from "app/client/models/rowset";
import {TableData} from "app/client/models/TableData"; import {TableData} from "app/client/models/TableData";
import * as TableModel from "app/client/models/TableModel"; import * as TableModel from "app/client/models/TableModel";
import {CellValue} from "app/common/DocActions";
namespace DataTableModel { namespace DataTableModel {
interface LazyArrayModel<T> extends KoArray<T | null> { interface LazyArrayModel<T> extends KoArray<T | null> {

View File

@ -1,7 +1,7 @@
/** /**
* Implements an autocomplete dropdown. * Implements an autocomplete dropdown.
*/ */
import {createPopper, Instance as Popper, Modifier, Options as PopperOptions} from '@popperjs/core'; import {createPopper, Modifier, Instance as Popper, Options as PopperOptions} from '@popperjs/core';
import {ACItem, ACResults, HighlightFunc} from 'app/client/lib/ACIndex'; import {ACItem, ACResults, HighlightFunc} from 'app/client/lib/ACIndex';
import {reportError} from 'app/client/models/errors'; import {reportError} from 'app/client/models/errors';
import {Disposable, dom, EventCB, IDisposable} from 'grainjs'; import {Disposable, dom, EventCB, IDisposable} from 'grainjs';

View File

@ -174,7 +174,7 @@ export class BillingModelImpl extends Disposable implements BillingModel {
await this._billingAPI.updateAddress(newAddr || undefined, newSettings || undefined); await this._billingAPI.updateAddress(newAddr || undefined, newSettings || undefined);
} }
// If there is an org update, re-initialize the org in the client. // If there is an org update, re-initialize the org in the client.
if (newSettings) { await this._appModel.topAppModel.initialize(); } if (newSettings) { this._appModel.topAppModel.initialize(); }
} else { } else {
throw new Error('BillingPage _submit error: no task in progress'); throw new Error('BillingPage _submit error: no task in progress');
} }

View File

@ -27,13 +27,6 @@ const ROW_ID_SKIP = -1;
* This should be the only part of the code that knows that. * This should be the only part of the code that knows that.
*/ */
export class ExtraRows { export class ExtraRows {
readonly leftTableDelta?: TableDelta;
readonly rightTableDelta?: TableDelta;
readonly rightAddRows: Set<number>;
readonly rightRemoveRows: Set<number>;
readonly leftAddRows: Set<number>;
readonly leftRemoveRows: Set<number>;
/** /**
* Map back from a possibly synthetic row id to an original strictly-positive row id. * Map back from a possibly synthetic row id to an original strictly-positive row id.
*/ */
@ -44,7 +37,14 @@ export class ExtraRows {
return { type: 'local-remove', id: -(rowId + 2) / 2 }; return { type: 'local-remove', id: -(rowId + 2) / 2 };
} }
public constructor(readonly tableId: string, readonly comparison?: DocStateComparisonDetails) { public readonly leftTableDelta?: TableDelta;
public readonly rightTableDelta?: TableDelta;
public readonly rightAddRows: Set<number>;
public readonly rightRemoveRows: Set<number>;
public readonly leftAddRows: Set<number>;
public readonly leftRemoveRows: Set<number>;
public constructor(public readonly tableId: string, public readonly comparison?: DocStateComparisonDetails) {
const remoteTableId = getRemoteTableId(tableId, comparison); const remoteTableId = getRemoteTableId(tableId, comparison);
this.leftTableDelta = this.comparison?.leftChanges?.tableDeltas[tableId]; this.leftTableDelta = this.comparison?.leftChanges?.tableDeltas[tableId];
if (remoteTableId) { if (remoteTableId) {

View File

@ -100,7 +100,7 @@ export class DocData extends BaseDocData {
this._nextDesc = options.description; this._nextDesc = options.description;
this._lastActionNum = null; this._lastActionNum = null;
this._triggerBundleFinalize = triggerFinalize; this._triggerBundleFinalize = triggerFinalize;
await prepareResolve(options.prepare()); prepareResolve(options.prepare());
this._shouldIncludeInBundle = options.shouldIncludeInBundle; this._shouldIncludeInBundle = options.shouldIncludeInBundle;
await triggerFinalizePromise; await triggerFinalizePromise;
@ -162,7 +162,7 @@ export class DocData extends BaseDocData {
public sendActions(actions: UserAction[], optDesc?: string): Promise<any[]> { public sendActions(actions: UserAction[], optDesc?: string): Promise<any[]> {
// Some old code relies on this promise being a bluebird Promise. // Some old code relies on this promise being a bluebird Promise.
// TODO Remove bluebird and this cast. // TODO Remove bluebird and this cast.
return bluebird.Promise.resolve(this._sendActionsImpl(actions, optDesc)) as any; return bluebird.Promise.resolve(this._sendActionsImpl(actions, optDesc)) as unknown as Promise<any[]>;
} }
/** /**

View File

@ -94,7 +94,8 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
public readonly isReadonly = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isReadonly : false); public readonly isReadonly = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isReadonly : false);
public readonly isPrefork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isPreFork : false); public readonly isPrefork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isPreFork : false);
public readonly isFork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isFork : false); public readonly isFork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isFork : false);
public readonly isRecoveryMode = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isRecoveryMode : false); public readonly isRecoveryMode = Computed.create(this, this.currentDoc,
(use, doc) => doc ? doc.isRecoveryMode : false);
public readonly userOverride = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.userOverride : null); public readonly userOverride = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.userOverride : null);
public readonly isBareFork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isBareFork : false); public readonly isBareFork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isBareFork : false);
public readonly isSample = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isSample : false); public readonly isSample = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isSample : false);
@ -133,8 +134,9 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
if (!urlId) { if (!urlId) {
this._openerHolder.clear(); this._openerHolder.clear();
} else { } else {
FlowRunner.create(this._openerHolder, (flow: AsyncFlow) => this._openDoc(flow, urlId, urlOpenMode, FlowRunner.create(this._openerHolder,
state.params?.compare, linkParameters)) (flow: AsyncFlow) => this._openDoc(flow, urlId, urlOpenMode, state.params?.compare, linkParameters)
)
.resultPromise.catch(err => this._onOpenError(err)); .resultPromise.catch(err => this._onOpenError(err));
} }
} }
@ -305,8 +307,10 @@ function addMenu(importSources: ImportSource[], gristDoc: GristDoc, isReadonly:
menuIcon("Widget"), "Add Widget to Page", testId('dp-add-widget-to-page'), menuIcon("Widget"), "Add Widget to Page", testId('dp-add-widget-to-page'),
dom.cls('disabled', isReadonly) dom.cls('disabled', isReadonly)
), ),
menuItem(() => gristDoc.addEmptyTable().catch(reportError), menuIcon("TypeTable"), "Add Empty Table", testId('dp-empty-table'), menuItem(() => gristDoc.addEmptyTable().catch(reportError),
dom.cls('disabled', isReadonly)), menuIcon("TypeTable"), "Add Empty Table", testId('dp-empty-table'),
dom.cls('disabled', isReadonly)
),
menuDivider(), menuDivider(),
...importSources.map((importSource, i) => ...importSources.map((importSource, i) =>
menuItem(importSource.action, menuItem(importSource.action,

View File

@ -314,7 +314,7 @@ function convertQueryToRefs(docModel: DocModel, query: Query): QueryRefs {
const tableRec: any = docModel.dataTables[query.tableId].tableMetaRow; const tableRec: any = docModel.dataTables[query.tableId].tableMetaRow;
const colRefsByColId: {[colId: string]: number} = {}; const colRefsByColId: {[colId: string]: number} = {};
for (const col of (tableRec as any).columns.peek().peek()) { for (const col of tableRec.columns.peek().peek()) {
colRefsByColId[col.colId.peek()] = col.getRowId(); colRefsByColId[col.colId.peek()] = col.getRowId();
} }

View File

@ -84,7 +84,7 @@ export class SearchModelImpl extends Disposable implements SearchModel {
// Listen to input value changes (debounced) to activate searching. // Listen to input value changes (debounced) to activate searching.
const findFirst = debounce((_value: string) => this._findFirst(_value), 100); const findFirst = debounce((_value: string) => this._findFirst(_value), 100);
this.autoDispose(this.value.addListener(v => { findFirst(v); })); this.autoDispose(this.value.addListener(v => { void findFirst(v); }));
} }
public async findNext() { public async findNext() {

View File

@ -7,7 +7,7 @@ import {DocData} from 'app/client/models/DocData';
import {DocAction, ReplaceTableData, TableDataAction, UserAction} from 'app/common/DocActions'; import {DocAction, ReplaceTableData, TableDataAction, UserAction} from 'app/common/DocActions';
import {isRaisedException} from 'app/common/gristTypes'; import {isRaisedException} from 'app/common/gristTypes';
import {countIf} from 'app/common/gutil'; import {countIf} from 'app/common/gutil';
import {ColTypeMap, TableData as BaseTableData} from 'app/common/TableData'; import {TableData as BaseTableData, ColTypeMap} from 'app/common/TableData';
import {BaseFormatter} from 'app/common/ValueFormatter'; import {BaseFormatter} from 'app/common/ValueFormatter';
import {Emitter} from 'grainjs'; import {Emitter} from 'grainjs';

View File

@ -153,7 +153,7 @@ class BillingPaymentForm extends BillingSubForm {
}) { }) {
super(); super();
const autofill = this._options.autofill; const autofill = this._options.autofill;
const stripeAPIKey = (G.window as any).gristConfig.stripeAPIKey; const stripeAPIKey = G.window.gristConfig.stripeAPIKey;
try { try {
this._stripe = G.Stripe(stripeAPIKey); this._stripe = G.Stripe(stripeAPIKey);
this._elements = this._stripe.elements(); this._elements = this._stripe.elements();
@ -462,7 +462,7 @@ function checkRequired(propertyName: string) {
// if the current observable value is valid. // if the current observable value is valid.
function createValidated( function createValidated(
owner: IDisposableOwnerT<any>, owner: IDisposableOwnerT<any>,
checkValidity: (value: string) => void checkValidity: (value: string) => void|Promise<void>,
): IValidated<string> { ): IValidated<string> {
const value = Observable.create(owner, ''); const value = Observable.create(owner, '');
const isInvalid = Observable.create<boolean>(owner, false); const isInvalid = Observable.create<boolean>(owner, false);

View File

@ -352,7 +352,7 @@ function getFieldNewPosition(fields: KoArray<ViewFieldRec>, item: IField,
return tableUtil.fieldInsertPositions(fields, index, 1)[0]; return tableUtil.fieldInsertPositions(fields, index, 1)[0];
} }
function getItemIndex<T>(collection: KoArray<ViewFieldRec>, item: ViewFieldRec|null): number { function getItemIndex(collection: KoArray<ViewFieldRec>, item: ViewFieldRec|null): number {
if (item !== null) { if (item !== null) {
return collection.peek().indexOf(item); return collection.peek().indexOf(item);
} }

View File

@ -194,8 +194,8 @@ export const cssHideForNarrowScreen = styled('div', `
* Attaches the global css properties to the document's root to them available in the page. * Attaches the global css properties to the document's root to them available in the page.
*/ */
export function attachCssRootVars(productFlavor: ProductFlavor, varsOnly: boolean = false) { export function attachCssRootVars(productFlavor: ProductFlavor, varsOnly: boolean = false) {
dom.update(document.documentElement!, varsOnly ? dom.cls(cssVarsOnly.className) : dom.cls(cssRootVars)); dom.update(document.documentElement, varsOnly ? dom.cls(cssVarsOnly.className) : dom.cls(cssRootVars));
document.documentElement!.classList.add(cssRoot.className); document.documentElement.classList.add(cssRoot.className);
document.body.classList.add(cssBody.className); document.body.classList.add(cssBody.className);
const theme = getTheme(productFlavor); const theme = getTheme(productFlavor);
if (theme.bodyClassName) { if (theme.bodyClassName) {

View File

@ -4,7 +4,7 @@ import { CellValue } from 'app/common/DocActions';
import { isVersions } from 'app/common/gristTypes'; import { isVersions } from 'app/common/gristTypes';
import { inlineStyle } from 'app/common/gutil'; import { inlineStyle } from 'app/common/gutil';
import { BaseFormatter } from 'app/common/ValueFormatter'; import { BaseFormatter } from 'app/common/ValueFormatter';
import { Diff, DIFF_DELETE, DIFF_INSERT, diff_match_patch as DiffMatchPatch, DIFF_EQUAL } from 'diff-match-patch'; import { Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch as DiffMatchPatch } from 'diff-match-patch';
import { Computed, dom } from 'grainjs'; import { Computed, dom } from 'grainjs';
/** /**

View File

@ -27,7 +27,7 @@ import * as gristTypes from 'app/common/gristTypes';
import * as gutil from 'app/common/gutil'; import * as gutil from 'app/common/gutil';
import { CellValue } from 'app/plugin/GristData'; import { CellValue } from 'app/plugin/GristData';
import { delay } from 'bluebird'; import { delay } from 'bluebird';
import { Computed, Disposable, dom as grainjsDom, fromKo, Holder, IDisposable, makeTestId } from 'grainjs'; import { Computed, Disposable, fromKo, dom as grainjsDom, Holder, IDisposable, makeTestId } from 'grainjs';
import * as ko from 'knockout'; import * as ko from 'knockout';
import * as _ from 'underscore'; import * as _ from 'underscore';
@ -77,7 +77,7 @@ export class FieldBuilder extends Disposable {
private readonly widgetCons: ko.Computed<{create: (...args: any[]) => NewAbstractWidget}>; private readonly widgetCons: ko.Computed<{create: (...args: any[]) => NewAbstractWidget}>;
private readonly docModel: DocModel; private readonly docModel: DocModel;
public constructor(readonly gristDoc: GristDoc, readonly field: ViewFieldRec, public constructor(public readonly gristDoc: GristDoc, public readonly field: ViewFieldRec,
private _cursor: Cursor) { private _cursor: Cursor) {
super(); super();

View File

@ -315,7 +315,7 @@ function readAclRules(docData: DocData, {log, compile}: ReadAclOptions): ReadAcl
} }
for (const [resourceId, rules] of rulesByResource.entries()) { for (const [resourceId, rules] of rulesByResource.entries()) {
const resourceRec = resourcesTable.getRecord(resourceId as number); const resourceRec = resourcesTable.getRecord(resourceId);
if (!resourceRec) { if (!resourceRec) {
throw new Error(`ACLRule ${rules[0].id} refers to an invalid ACLResource ${resourceId}`); throw new Error(`ACLRule ${rules[0].id} refers to an invalid ACLResource ${resourceId}`);
continue; continue;

View File

@ -82,7 +82,8 @@ const SCHEMA_ACTIONS = new Set(['AddTable', 'RemoveTable', 'RenameTable', 'AddCo
/** /**
* Determines whether a given action is a schema action or not. * Determines whether a given action is a schema action or not.
*/ */
export function isSchemaAction(action: DocAction): action is AddTable | RemoveTable | RenameTable | AddColumn | RemoveColumn | RenameColumn | ModifyColumn { export function isSchemaAction(action: DocAction):
action is AddTable | RemoveTable | RenameTable | AddColumn | RemoveColumn | RenameColumn | ModifyColumn {
return SCHEMA_ACTIONS.has(action[0]); return SCHEMA_ACTIONS.has(action[0]);
} }

View File

@ -48,7 +48,7 @@ export interface OpenLocalDocResult {
userOverride?: UserOverride; userOverride?: UserOverride;
} }
export class UserOverride { export interface UserOverride {
user: FullUser|null; user: FullUser|null;
access: Role|null; access: Role|null;
} }

View File

@ -76,7 +76,7 @@ export abstract class BaseComponent implements IForwarderDest {
public async forwardMessage(msg: IMsgCustom): Promise<any> { public async forwardMessage(msg: IMsgCustom): Promise<any> {
if (!this._activated) { await this.activate(); } if (!this._activated) { await this.activate(); }
this.inactivityTimer.ping(); this.inactivityTimer.ping();
this.doForwardMessage(msg); // tslint:disable-line:no-floating-promises TODO this.doForwardMessage(msg); // eslint-disable-line @typescript-eslint/no-floating-promises
} }
protected abstract doForwardCall(c: IMsgRpcCall): Promise<any>; protected abstract doForwardCall(c: IMsgRpcCall): Promise<any>;

View File

@ -629,7 +629,7 @@ export class UserAPIImpl extends BaseAPI implements UserAPI {
} }
export class DocWorkerAPIImpl extends BaseAPI implements DocWorkerAPI { export class DocWorkerAPIImpl extends BaseAPI implements DocWorkerAPI {
constructor(readonly url: string, _options: IOptions = {}) { constructor(public readonly url: string, _options: IOptions = {}) {
super(_options); super(_options);
} }
@ -682,7 +682,7 @@ export class DocWorkerAPIImpl extends BaseAPI implements DocWorkerAPI {
export class DocAPIImpl extends BaseAPI implements DocAPI { export class DocAPIImpl extends BaseAPI implements DocAPI {
private _url: string; private _url: string;
constructor(url: string, readonly docId: string, options: IOptions = {}) { constructor(url: string, public readonly docId: string, options: IOptions = {}) {
super(options); super(options);
this._url = `${url}/api/docs/${docId}`; this._url = `${url}/api/docs/${docId}`;
} }

View File

@ -8,7 +8,9 @@
export function tbind<T, R, Args extends any[]>(func: (this: T, ...a: Args) => R, context: T): (...a: Args) => R; export function tbind<T, R, Args extends any[]>(func: (this: T, ...a: Args) => R, context: T): (...a: Args) => R;
// Bind context and first arg for a function of up to 5 args. // Bind context and first arg for a function of up to 5 args.
export function tbind<T, R, X, Args extends any[]>(func: (this: T, x: X, ...a: Args) => R, context: T, x: X): (...a: Args) => R; export function tbind<T, R, X, Args extends any[]>(
func: (this: T, x: X, ...a: Args) => R, context: T, x: X
): (...a: Args) => R;
export function tbind(func: any, context: any, ...boundArgs: any[]): any { export function tbind(func: any, context: any, ...boundArgs: any[]): any {
return func.bind(context, ...boundArgs); return func.bind(context, ...boundArgs);

View File

@ -12,7 +12,8 @@ type PromisifiedFunction<T extends AnyFunction> =
T extends (a1: infer A1) => infer U ? (a1: A1) => Promise<Unpacked<U>> : T extends (a1: infer A1) => infer U ? (a1: A1) => Promise<Unpacked<U>> :
T extends (a1: infer A1, a2: infer A2) => infer U ? (a1: A1, a2: A2) => Promise<Unpacked<U>> : T extends (a1: infer A1, a2: infer A2) => infer U ? (a1: A1, a2: A2) => Promise<Unpacked<U>> :
T extends (a1: infer A1, a2: infer A2, a3: infer A3) => infer U ? (a1: A1, a2: A2, a3: A3) => Promise<Unpacked<U>> : T extends (a1: infer A1, a2: infer A2, a3: infer A3) => infer U ? (a1: A1, a2: A2, a3: A3) => Promise<Unpacked<U>> :
T extends (a1: infer A1, a2: infer A2, a3: infer A3, a4: infer A4) => infer U ? (a1: A1, a2: A2, a3: A3, a4: A4) => Promise<Unpacked<U>> : T extends (a1: infer A1, a2: infer A2, a3: infer A3, a4: infer A4) =>
infer U ? (a1: A1, a2: A2, a3: A3, a4: A4) => Promise<Unpacked<U>> :
// ... // ...
T extends (...args: any[]) => infer U ? (...args: any[]) => Promise<Unpacked<U>> : T; T extends (...args: any[]) => infer U ? (...args: any[]) => Promise<Unpacked<U>> : T;

View File

@ -55,7 +55,9 @@ export class DocApiForwarder {
app.use('^/api/docs$', withoutDoc); app.use('^/api/docs$', withoutDoc);
} }
private async _forwardToDocWorker(withDocId: boolean, role: 'viewers'|null, req: express.Request, res: express.Response): Promise<void> { private async _forwardToDocWorker(
withDocId: boolean, role: 'viewers'|null, req: express.Request, res: express.Response,
): Promise<void> {
let docId: string|null = null; let docId: string|null = null;
if (withDocId) { if (withDocId) {
const docAuth = await getOrSetDocAuth(req as RequestWithLogin, this._dbManager, req.params.docId); const docAuth = await getOrSetDocAuth(req as RequestWithLogin, this._dbManager, req.params.docId);

View File

@ -424,7 +424,7 @@ export class DocWorkerMap implements IDocWorkerMap {
} }
public async updateDocStatus(docId: string, checksum: string): Promise<void> { public async updateDocStatus(docId: string, checksum: string): Promise<void> {
this.updateChecksum('doc', docId, checksum); return this.updateChecksum('doc', docId, checksum);
} }
public async updateChecksum(family: string, key: string, checksum: string) { public async updateChecksum(family: string, key: string, checksum: string) {

View File

@ -8,8 +8,8 @@ import {checkSubdomainValidity} from 'app/common/orgNameUtils';
import * as roles from 'app/common/roles'; import * as roles from 'app/common/roles';
// TODO: API should implement UserAPI // TODO: API should implement UserAPI
import {ANONYMOUS_USER_EMAIL, DocumentProperties, EVERYONE_EMAIL, import {ANONYMOUS_USER_EMAIL, DocumentProperties, EVERYONE_EMAIL,
ManagerDelta, NEW_DOCUMENT_CODE, Organization as OrgInfo, ManagerDelta, NEW_DOCUMENT_CODE, OrganizationProperties,
OrganizationProperties, PermissionData, PermissionDelta, SUPPORT_EMAIL, UserAccessData, Organization as OrgInfo, PermissionData, PermissionDelta, SUPPORT_EMAIL, UserAccessData,
WorkspaceProperties} from "app/common/UserAPI"; WorkspaceProperties} from "app/common/UserAPI";
import {AclRule, AclRuleDoc, AclRuleOrg, AclRuleWs} from "app/gen-server/entity/AclRule"; import {AclRule, AclRuleDoc, AclRuleOrg, AclRuleWs} from "app/gen-server/entity/AclRule";
import {Alias} from "app/gen-server/entity/Alias"; import {Alias} from "app/gen-server/entity/Alias";
@ -1903,7 +1903,7 @@ export class HomeDBManager extends EventEmitter {
name: u.name, name: u.name,
email: u.logins.map((login: Login) => login.displayEmail)[0], email: u.logins.map((login: Login) => login.displayEmail)[0],
picture: u.picture, picture: u.picture,
access: userRoleMap[u.id] as roles.Role access: userRoleMap[u.id]
})); }));
return { return {
status: 200, status: 200,
@ -2811,7 +2811,7 @@ export class HomeDBManager extends EventEmitter {
let effectiveUserId = userId; let effectiveUserId = userId;
let threshold = options.markPermissions; let threshold = options.markPermissions;
if (options.allowSpecialPermit && scope.specialPermit && scope.specialPermit.docId) { if (options.allowSpecialPermit && scope.specialPermit && scope.specialPermit.docId) {
query = query.andWhere('docs.id = :docId', {docId: scope.specialPermit.docId!}); query = query.andWhere('docs.id = :docId', {docId: scope.specialPermit.docId});
effectiveUserId = this.getPreviewerUserId(); effectiveUserId = this.getPreviewerUserId();
threshold = Permissions.VIEW; threshold = Permissions.VIEW;
} }
@ -3051,7 +3051,7 @@ export class HomeDBManager extends EventEmitter {
if (!groupProps.orgOnly || !inherit) { if (!groupProps.orgOnly || !inherit) {
// Skip this group if it's an org only group and the resource inherits from a parent. // Skip this group if it's an org only group and the resource inherits from a parent.
const group = new Group(); const group = new Group();
group.name = groupProps.name as roles.Role; group.name = groupProps.name;
if (inherit) { if (inherit) {
this._setInheritance(group, inherit); this._setInheritance(group, inherit);
} }
@ -3333,7 +3333,10 @@ export class HomeDBManager extends EventEmitter {
`${everyoneId} IN (gu0.user_id, gu1.user_id, gu2.user_id, gu3.user_id) ` + `${everyoneId} IN (gu0.user_id, gu1.user_id, gu2.user_id, gu3.user_id) ` +
`then ${everyoneContribution} else (case when ` + `then ${everyoneContribution} else (case when ` +
`${anonId} IN (gu0.user_id, gu1.user_id, gu2.user_id, gu3.user_id) ` + `${anonId} IN (gu0.user_id, gu1.user_id, gu2.user_id, gu3.user_id) ` +
`then ${Permissions.PUBLIC} | acl_rules.permissions else acl_rules.permissions end) end)`, 8), 'permissions'); `then ${Permissions.PUBLIC} | acl_rules.permissions else acl_rules.permissions end) end)`, 8
),
'permissions'
);
} }
q = q.from('acl_rules', 'acl_rules'); q = q.from('acl_rules', 'acl_rules');
q = this._getUsersAcls(q, users, accessStyle); q = this._getUsersAcls(q, users, accessStyle);

View File

@ -210,7 +210,9 @@ export class Housekeeper {
// Call a document endpoint with a permit, cleaning up after the call. // Call a document endpoint with a permit, cleaning up after the call.
// Checks that the user is the support user. // Checks that the user is the support user.
private _withSupport(callback: (docId: string, headers: Record<string, string>) => Promise<Fetch.Response>): express.RequestHandler { private _withSupport(
callback: (docId: string, headers: Record<string, string>) => Promise<Fetch.Response>
): express.RequestHandler {
return expressWrap(async (req, res) => { return expressWrap(async (req, res) => {
const userId = getAuthorizedUserId(req); const userId = getAuthorizedUserId(req);
if (userId !== this._dbManager.getSupportUserId()) { if (userId !== this._dbManager.getSupportUserId()) {

View File

@ -133,7 +133,7 @@ export async function addImporter(name: string, path: string, mode: 'fullscreen'
*/ */
export function ready(): void { export function ready(): void {
rpc.processIncoming(); rpc.processIncoming();
rpc.sendReadyMessage(); void rpc.sendReadyMessage();
} }
function getPluginPath(location: Location) { function getPluginPath(location: Location) {

View File

@ -715,7 +715,7 @@ export class ActiveDoc extends EventEmitter {
*/ */
public async getFormulaError(docSession: DocSession, tableId: string, colId: string, public async getFormulaError(docSession: DocSession, tableId: string, colId: string,
rowId: number): Promise<CellValue> { rowId: number): Promise<CellValue> {
if (!this._granularAccess.hasTableAccess(docSession, tableId)) { return null; } if (!await this._granularAccess.hasTableAccess(docSession, tableId)) { return null; }
this.logInfo(docSession, "getFormulaError(%s, %s, %s, %s)", this.logInfo(docSession, "getFormulaError(%s, %s, %s, %s)",
docSession, tableId, colId, rowId); docSession, tableId, colId, rowId);
await this.waitForInitialization(); await this.waitForInitialization();
@ -1094,7 +1094,7 @@ export class ActiveDoc extends EventEmitter {
]); ]);
// Migrate the document if needed. // Migrate the document if needed.
const values = marshal.loads(docInfoData!); const values = marshal.loads(docInfoData);
const versionCol = values.schemaVersion; const versionCol = values.schemaVersion;
const docSchemaVersion = (versionCol && versionCol.length === 1 ? versionCol[0] : 0); const docSchemaVersion = (versionCol && versionCol.length === 1 ? versionCol[0] : 0);
if (docSchemaVersion < schemaVersion) { if (docSchemaVersion < schemaVersion) {
@ -1126,7 +1126,7 @@ export class ActiveDoc extends EventEmitter {
const tableNames: string[] = await this._rawPyCall('load_meta_tables', tablesData, columnsData); const tableNames: string[] = await this._rawPyCall('load_meta_tables', tablesData, columnsData);
// Figure out which tables are on-demand. // Figure out which tables are on-demand.
const tablesParsed: BulkColValues = marshal.loads(tablesData!); const tablesParsed: BulkColValues = marshal.loads(tablesData);
const onDemandMap = zipObject(tablesParsed.tableId as string[], tablesParsed.onDemand); const onDemandMap = zipObject(tablesParsed.tableId as string[], tablesParsed.onDemand);
const onDemandNames = remove(tableNames, (t) => onDemandMap[t]); const onDemandNames = remove(tableNames, (t) => onDemandMap[t]);
@ -1183,7 +1183,7 @@ export class ActiveDoc extends EventEmitter {
const result: ApplyUAResult = await new Promise<ApplyUAResult>( const result: ApplyUAResult = await new Promise<ApplyUAResult>(
(resolve, reject) => (resolve, reject) =>
this._sharing!.addUserAction({action, docSession, resolve, reject})); this._sharing.addUserAction({action, docSession, resolve, reject}));
this.logDebug(docSession, "_applyUserActions returning %s", shortDesc(result)); this.logDebug(docSession, "_applyUserActions returning %s", shortDesc(result));
if (result.isModification) { if (result.isModification) {

View File

@ -4,7 +4,7 @@
* of the client-side code. * of the client-side code.
*/ */
import * as express from 'express'; import * as express from 'express';
import fetch, {RequestInit, Response as FetchResponse} from 'node-fetch'; import fetch, {Response as FetchResponse, RequestInit} from 'node-fetch';
import {ApiError} from 'app/common/ApiError'; import {ApiError} from 'app/common/ApiError';
import {getSlugIfNeeded, parseSubdomainStrictly} from 'app/common/gristUrls'; import {getSlugIfNeeded, parseSubdomainStrictly} from 'app/common/gristUrls';

View File

@ -215,10 +215,9 @@ export class DocWorkerApi {
// Initiate a fork. Used internally to implement ActiveDoc.fork. Only usable via a Permit. // Initiate a fork. Used internally to implement ActiveDoc.fork. Only usable via a Permit.
this._app.post('/api/docs/:docId/create-fork', canEdit, throttled(async (req, res) => { this._app.post('/api/docs/:docId/create-fork', canEdit, throttled(async (req, res) => {
const mreq = req as RequestWithLogin;
const docId = stringParam(req.params.docId); const docId = stringParam(req.params.docId);
const srcDocId = stringParam(req.body.srcDocId); const srcDocId = stringParam(req.body.srcDocId);
if (srcDocId !== mreq.specialPermit?.otherDocId) { throw new Error('access denied'); } if (srcDocId !== req.specialPermit?.otherDocId) { throw new Error('access denied'); }
await this._docManager.storageManager.prepareFork(srcDocId, docId); await this._docManager.storageManager.prepareFork(srcDocId, docId);
res.json({srcDocId, docId}); res.json({srcDocId, docId});
})); }));
@ -249,7 +248,7 @@ export class DocWorkerApi {
this._app.post('/api/docs/:docId/recover', canEdit, throttled(async (req, res) => { this._app.post('/api/docs/:docId/recover', canEdit, throttled(async (req, res) => {
const recoveryModeRaw = req.body.recoveryMode; const recoveryModeRaw = req.body.recoveryMode;
const recoveryMode = (typeof recoveryModeRaw === 'boolean') ? recoveryModeRaw : undefined; const recoveryMode = (typeof recoveryModeRaw === 'boolean') ? recoveryModeRaw : undefined;
if (!this._isOwner(req)) { throw new Error('Only owners can control recovery mode'); } if (!await this._isOwner(req)) { throw new Error('Only owners can control recovery mode'); }
const activeDoc = await this._docManager.fetchDoc(docSessionFromRequest(req), getDocId(req), recoveryMode); const activeDoc = await this._docManager.fetchDoc(docSessionFromRequest(req), getDocId(req), recoveryMode);
res.json({ res.json({
recoveryMode: activeDoc.recoveryMode recoveryMode: activeDoc.recoveryMode

View File

@ -16,7 +16,8 @@ import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
import {assertAccess, Authorizer, DocAuthorizer, DummyAuthorizer, import {assertAccess, Authorizer, DocAuthorizer, DummyAuthorizer,
isSingleUserMode} from 'app/server/lib/Authorizer'; isSingleUserMode} from 'app/server/lib/Authorizer';
import {Client} from 'app/server/lib/Client'; import {Client} from 'app/server/lib/Client';
import {getDocSessionCachedDoc, makeExceptionalDocSession, makeOptDocSession, OptDocSession} from 'app/server/lib/DocSession'; import {getDocSessionCachedDoc, makeExceptionalDocSession, makeOptDocSession} from 'app/server/lib/DocSession';
import {OptDocSession} from 'app/server/lib/DocSession';
import * as docUtils from 'app/server/lib/docUtils'; import * as docUtils from 'app/server/lib/docUtils';
import {GristServer} from 'app/server/lib/GristServer'; import {GristServer} from 'app/server/lib/GristServer';
import {IDocStorageManager} from 'app/server/lib/IDocStorageManager'; import {IDocStorageManager} from 'app/server/lib/IDocStorageManager';
@ -498,7 +499,7 @@ export class DocManager extends EventEmitter {
// TODO: We should be skeptical of the upload file to close a possible // TODO: We should be skeptical of the upload file to close a possible
// security vulnerability. See https://phab.getgrist.com/T457. // security vulnerability. See https://phab.getgrist.com/T457.
const docName = await this._createNewDoc(id); const docName = await this._createNewDoc(id);
const docPath = await this.storageManager.getPath(docName); const docPath: string = this.storageManager.getPath(docName);
await docUtils.copyFile(uploadInfo.files[0].absPath, docPath); await docUtils.copyFile(uploadInfo.files[0].absPath, docPath);
await this.storageManager.addToStorage(docName); await this.storageManager.addToStorage(docName);
return {title: basename, id: docName}; return {title: basename, id: docName};

View File

@ -65,7 +65,12 @@ export class DocPluginManager {
private _pluginInstances: PluginInstance[]; private _pluginInstances: PluginInstance[];
constructor(private _localPlugins: LocalPlugin[], private _appRoot: string, private _activeDoc: ActiveDoc, private _server: GristServer) { constructor(
private _localPlugins: LocalPlugin[],
private _appRoot: string,
private _activeDoc: ActiveDoc,
private _server: GristServer
) {
this.gristDocAPI = new GristDocAPIImpl(_activeDoc); this.gristDocAPI = new GristDocAPIImpl(_activeDoc);
this._pluginInstances = []; this._pluginInstances = [];
this.ready = this._initialize(); this.ready = this._initialize();

View File

@ -439,7 +439,9 @@ export class DocStorage implements ISQLiteDB {
* Note that SQLite may contain tables that aren't used for Grist data (e.g. attachments), for * Note that SQLite may contain tables that aren't used for Grist data (e.g. attachments), for
* which such encoding/marshalling is not used, and e.g. binary data is stored to BLOBs directly. * which such encoding/marshalling is not used, and e.g. binary data is stored to BLOBs directly.
*/ */
private static _encodeValue(marshaller: marshal.Marshaller, sqlType: string, val: any): Uint8Array|string|number|boolean { private static _encodeValue(
marshaller: marshal.Marshaller, sqlType: string, val: any
): Uint8Array|string|number|boolean {
const marshalled = () => { const marshalled = () => {
marshaller.marshal(val); marshaller.marshal(val);
return marshaller.dump(); return marshaller.dump();
@ -1376,7 +1378,7 @@ export class DocStorage implements ISQLiteDB {
// columns, or adding or removing or changing default values on a column." // columns, or adding or removing or changing default values on a column."
const row = await this.get("PRAGMA schema_version"); const row = await this.get("PRAGMA schema_version");
assert(row && row.schema_version, "Could not retrieve schema_version."); assert(row && row.schema_version, "Could not retrieve schema_version.");
const newSchemaVersion = row!.schema_version + 1; const newSchemaVersion = row.schema_version + 1;
const tmpTableId = DocStorage._makeTmpTableId(tableId); const tmpTableId = DocStorage._makeTmpTableId(tableId);
await this._getDB().runEach( await this._getDB().runEach(
"PRAGMA writable_schema=ON", "PRAGMA writable_schema=ON",
@ -1462,7 +1464,7 @@ export class DocStorage implements ISQLiteDB {
* should be reasonably fast: * should be reasonably fast:
* https://sqlite.org/tempfiles.html#temp_databases * https://sqlite.org/tempfiles.html#temp_databases
*/ */
public async _fetchQueryWithManyParameters(query: ExpandedQuery): Promise<Buffer> { private async _fetchQueryWithManyParameters(query: ExpandedQuery): Promise<Buffer> {
const db = this._getDB(); const db = this._getDB();
return db.execTransaction(async () => { return db.execTransaction(async () => {
const tableNames: string[] = []; const tableNames: string[] = [];
@ -1490,7 +1492,7 @@ export class DocStorage implements ISQLiteDB {
* Construct SQL for an ExpandedQuery. Expects that filters have been converted into * Construct SQL for an ExpandedQuery. Expects that filters have been converted into
* a set of WHERE terms that should be ANDed. * a set of WHERE terms that should be ANDed.
*/ */
public _getSqlForQuery(query: ExpandedQuery, whereParts: string[]) { private _getSqlForQuery(query: ExpandedQuery, whereParts: string[]) {
const whereClause = whereParts.length > 0 ? `WHERE ${whereParts.join(' AND ')}` : ''; const whereClause = whereParts.length > 0 ? `WHERE ${whereParts.join(' AND ')}` : '';
const limitClause = (typeof query.limit === 'number') ? `LIMIT ${query.limit}` : ''; const limitClause = (typeof query.limit === 'number') ? `LIMIT ${query.limit}` : '';
const joinClauses = query.joins ? query.joins.join(' ') : ''; const joinClauses = query.joins ? query.joins.join(' ') : '';

View File

@ -196,7 +196,7 @@ export class DocStorageManager implements IDocStorageManager {
* Electron version only. Shows the given doc in the file explorer. * Electron version only. Shows the given doc in the file explorer.
*/ */
public async showItemInFolder(docName: string): Promise<void> { public async showItemInFolder(docName: string): Promise<void> {
this._shell.showItemInFolder(await this.getPath(docName)); this._shell.showItemInFolder(this.getPath(docName));
} }
public async closeStorage() { public async closeStorage() {

View File

@ -133,7 +133,7 @@ export class KeyMappedExternalStorage implements ExternalStorage {
export class ChecksummedExternalStorage implements ExternalStorage { export class ChecksummedExternalStorage implements ExternalStorage {
private _closed: boolean = false; private _closed: boolean = false;
constructor(readonly label: string, private _ext: ExternalStorage, private _options: { constructor(public readonly label: string, private _ext: ExternalStorage, private _options: {
maxRetries: number, // how many time to retry inconsistent downloads maxRetries: number, // how many time to retry inconsistent downloads
initialDelayMs: number, // how long to wait before retrying initialDelayMs: number, // how long to wait before retrying
localHash: PropStorage, // key/value store for hashes of downloaded content localHash: PropStorage, // key/value store for hashes of downloaded content

View File

@ -145,7 +145,7 @@ export class FlexServer implements GristServer {
private _sendAppPage: (req: express.Request, resp: express.Response, options: ISendAppPageOptions) => Promise<void>; private _sendAppPage: (req: express.Request, resp: express.Response, options: ISendAppPageOptions) => Promise<void>;
constructor(public port: number, public name: string = 'flexServer', constructor(public port: number, public name: string = 'flexServer',
readonly options: FlexServerOptions = {}) { public readonly options: FlexServerOptions = {}) {
this.app = express(); this.app = express();
this.app.set('port', port); this.app.set('port', port);
this.appRoot = getAppRoot(); this.appRoot = getAppRoot();

View File

@ -724,7 +724,9 @@ export class GranularAccess implements GranularAccessForBundle {
const ruler = await this._getRuler(cursor); const ruler = await this._getRuler(cursor);
const tableId = getTableId(action); const tableId = getTableId(action);
const ruleSets = ruler.ruleCollection.getAllColumnRuleSets(tableId); const ruleSets = ruler.ruleCollection.getAllColumnRuleSets(tableId);
const colIds = new Set(([] as string[]).concat(...ruleSets.map(ruleSet => ruleSet.colIds === '*' ? [] : ruleSet.colIds))); const colIds = new Set(([] as string[]).concat(
...ruleSets.map(ruleSet => ruleSet.colIds === '*' ? [] : ruleSet.colIds)
));
const access = await ruler.getAccess(cursor.docSession); const access = await ruler.getAccess(cursor.docSession);
// Check columns in a consistent order, for determinism (easier testing). // Check columns in a consistent order, for determinism (easier testing).
// TODO: could pool some work between columns by doing them together rather than one by one. // TODO: could pool some work between columns by doing them together rather than one by one.
@ -1164,7 +1166,9 @@ export class GranularAccess implements GranularAccessForBundle {
const rowsBefore = cloneDeep(tableData?.getTableDataAction() || ['TableData', '', [], {}] as TableDataAction); const rowsBefore = cloneDeep(tableData?.getTableDataAction() || ['TableData', '', [], {}] as TableDataAction);
docData.receiveAction(docAction); docData.receiveAction(docAction);
// If table is deleted, state afterwards doesn't matter. // If table is deleted, state afterwards doesn't matter.
const rowsAfter = docData.getTable(tableId) ? cloneDeep(tableData?.getTableDataAction() || ['TableData', '', [], {}] as TableDataAction) : rowsBefore; const rowsAfter = docData.getTable(tableId) ?
cloneDeep(tableData?.getTableDataAction() || ['TableData', '', [], {}] as TableDataAction) :
rowsBefore;
const step: ActionStep = {action: docAction, rowsBefore, rowsAfter}; const step: ActionStep = {action: docAction, rowsBefore, rowsAfter};
steps.push(step); steps.push(step);
} }
@ -1208,7 +1212,7 @@ export class GranularAccess implements GranularAccessForBundle {
if (applied) { if (applied) {
// Rules may have changed - back them off to a copy of their original state. // Rules may have changed - back them off to a copy of their original state.
ruler = new Ruler(this); ruler = new Ruler(this);
ruler.update(metaDocData); await ruler.update(metaDocData);
} }
let replaceRuler = false; let replaceRuler = false;
for (const docAction of docActions) { for (const docAction of docActions) {
@ -1228,7 +1232,7 @@ export class GranularAccess implements GranularAccessForBundle {
replaceRuler = true; replaceRuler = true;
} else if (replaceRuler) { } else if (replaceRuler) {
ruler = new Ruler(this); ruler = new Ruler(this);
ruler.update(metaDocData); await ruler.update(metaDocData);
replaceRuler = false; replaceRuler = false;
} }
step.ruler = ruler; step.ruler = ruler;
@ -1625,7 +1629,8 @@ export class CensorshipInfo {
const tableId = tableRefToTableId.get(tableRef); const tableId = tableRefToTableId.get(tableRef);
if (!tableId) { throw new Error('table not found'); } if (!tableId) { throw new Error('table not found'); }
const colId = rec.get('colId') as string; const colId = rec.get('colId') as string;
if (this.censoredTables.has(tableRef) || (colId !== 'manualSort' && permInfo.getColumnAccess(tableId, colId).perms.read === 'deny')) { if (this.censoredTables.has(tableRef) ||
(colId !== 'manualSort' && permInfo.getColumnAccess(tableId, colId).perms.read === 'deny')) {
censoredColumnCodes.add(columnCode(tableRef, colId)); censoredColumnCodes.add(columnCode(tableRef, colId));
} }
} }

View File

@ -239,7 +239,7 @@ export class HostedStorageManager implements IDocStorageManager {
await this.prepareLocalDoc(docName, 'new'); await this.prepareLocalDoc(docName, 'new');
if (this._inventory) { if (this._inventory) {
await this._inventory.create(docName); await this._inventory.create(docName);
this._onInventoryChange(docName); await this._onInventoryChange(docName);
} }
this.markAsChanged(docName); this.markAsChanged(docName);
} }

View File

@ -10,18 +10,14 @@ export const ITestingHooks = t.iface([], {
"updateAuthToken": t.func("void", t.param("instId", "string"), t.param("authToken", "string")), "updateAuthToken": t.func("void", t.param("instId", "string"), t.param("authToken", "string")),
"getAuthToken": t.func(t.union("string", "null"), t.param("instId", "string")), "getAuthToken": t.func(t.union("string", "null"), t.param("instId", "string")),
"useTestToken": t.func("void", t.param("instId", "string"), t.param("token", "string")), "useTestToken": t.func("void", t.param("instId", "string"), t.param("token", "string")),
"setLoginSessionProfile": t.func("void", t.param("gristSidCookie", "string"), "setLoginSessionProfile": t.func("void", t.param("gristSidCookie", "string"), t.param("profile", t.union("UserProfile", "null")), t.param("org", "string", true)),
t.param("profile", t.union("UserProfile", "null")), t.param("org", "string", true)),
"setServerVersion": t.func("void", t.param("version", t.union("string", "null"))), "setServerVersion": t.func("void", t.param("version", t.union("string", "null"))),
"disconnectClients": t.func("void"), "disconnectClients": t.func("void"),
"commShutdown": t.func("void"), "commShutdown": t.func("void"),
"commRestart": t.func("void"), "commRestart": t.func("void"),
"commSetClientPersistence": t.func("void", t.param("ttlMs", "number")), "commSetClientPersistence": t.func("void", t.param("ttlMs", "number")),
"closeDocs": t.func("void"), "closeDocs": t.func("void"),
"setDocWorkerActivation": t.func("void", t.param("workerId", "string"), "setDocWorkerActivation": t.func("void", t.param("workerId", "string"), t.param("active", t.union(t.lit('active'), t.lit('inactive'), t.lit('crash')))),
t.param("active", t.union(t.lit('active'),
t.lit('inactive'),
t.lit('crash')))),
"flushAuthorizerCache": t.func("void"), "flushAuthorizerCache": t.func("void"),
"getDocClientCounts": t.func(t.array(t.tuple("string", "number"))), "getDocClientCounts": t.func(t.array(t.tuple("string", "number"))),
"setActiveDocTimeout": t.func("number", t.param("seconds", "number")), "setActiveDocTimeout": t.func("number", t.param("seconds", "number")),
@ -29,6 +25,5 @@ export const ITestingHooks = t.iface([], {
const exportedTypeSuite: t.ITypeSuite = { const exportedTypeSuite: t.ITypeSuite = {
ITestingHooks, ITestingHooks,
UserProfile: t.name("object"),
}; };
export default exportedTypeSuite; export default exportedTypeSuite;

View File

@ -1,8 +1,8 @@
import {UserProfile} from 'app/common/LoginSessionAPI'; import {UserProfile} from 'app/common/LoginSessionAPI';
export interface ITestingHooks { export interface ITestingHooks {
getOwnPort(): number; getOwnPort(): Promise<number>;
getPort(): number; getPort(): Promise<number>;
updateAuthToken(instId: string, authToken: string): Promise<void>; updateAuthToken(instId: string, authToken: string): Promise<void>;
getAuthToken(instId: string): Promise<string|null>; getAuthToken(instId: string): Promise<string|null>;
useTestToken(instId: string, token: string): Promise<void>; useTestToken(instId: string, token: string): Promise<void>;

View File

@ -62,7 +62,7 @@ export class OnDemandActions {
// Check that the actionType can be applied without the sandbox and also that the action // Check that the actionType can be applied without the sandbox and also that the action
// is on a data table. // is on a data table.
const isOnDemandAction = ACTION_TYPES.has(a[0] as string); const isOnDemandAction = ACTION_TYPES.has(a[0] as string);
const isDataTableAction = typeof a[1] === 'string' && !(a[1] as string).startsWith('_grist_'); const isDataTableAction = typeof a[1] === 'string' && !a[1].startsWith('_grist_');
if (a[0] === 'ApplyUndoActions') { if (a[0] === 'ApplyUndoActions') {
// Split actions inside the undo action array. // Split actions inside the undo action array.
const [undoNormal, undoOnDemand] = this.splitByOnDemand(a[1] as UserAction[]); const [undoNormal, undoOnDemand] = this.splitByOnDemand(a[1] as UserAction[]);

View File

@ -155,7 +155,7 @@ export class Sharing {
private async _rebaseLocalActions(): Promise<void> { private async _rebaseLocalActions(): Promise<void> {
const rebaseQueue: Deque<UserActionBundle> = new Deque<UserActionBundle>(); const rebaseQueue: Deque<UserActionBundle> = new Deque<UserActionBundle>();
try { try {
await this.createCheckpoint(); this.createCheckpoint();
const actions: LocalActionBundle[] = await this._actionHistory.fetchAllLocal(); const actions: LocalActionBundle[] = await this._actionHistory.fetchAllLocal();
assert(actions.length > 0); assert(actions.length > 0);
await this.doApplyUserActionBundle(this._createUndo(actions), null); await this.doApplyUserActionBundle(this._createUndo(actions), null);
@ -163,7 +163,7 @@ export class Sharing {
await this._actionHistory.clearLocalActions(); await this._actionHistory.clearLocalActions();
} catch (e) { } catch (e) {
log.error("Can't undo local actions; sharing is off"); log.error("Can't undo local actions; sharing is off");
await this.rollbackToCheckpoint(); this.rollbackToCheckpoint();
// TODO this.disconnect(); // TODO this.disconnect();
// TODO errorState = true; // TODO errorState = true;
return; return;
@ -185,11 +185,11 @@ export class Sharing {
} }
} }
if (rebaseFailures.length > 0) { if (rebaseFailures.length > 0) {
await this.createBackupAtCheckpoint(); this.createBackupAtCheckpoint();
// TODO we should notify the user too. // TODO we should notify the user too.
log.error('Rebase failed to reapply some of your actions, backup of local at...'); log.error('Rebase failed to reapply some of your actions, backup of local at...');
} }
await this.releaseCheckpoint(); this.releaseCheckpoint();
} }
// ====================================================================== // ======================================================================
@ -374,7 +374,9 @@ export class Sharing {
const docActions = getEnvContent(sandboxActionBundle.stored).concat( const docActions = getEnvContent(sandboxActionBundle.stored).concat(
getEnvContent(sandboxActionBundle.calc)); getEnvContent(sandboxActionBundle.calc));
const accessControl = this._activeDoc.getGranularAccessForBundle(docSession || makeExceptionalDocSession('share'), docActions, undo, userActions); const accessControl = this._activeDoc.getGranularAccessForBundle(
docSession || makeExceptionalDocSession('share'), docActions, undo, userActions
);
try { try {
// TODO: see if any of the code paths that have no docSession are relevant outside // TODO: see if any of the code paths that have no docSession are relevant outside
// of tests. // of tests.

View File

@ -6,13 +6,15 @@ import * as Comm from 'app/server/lib/Comm';
import {ILoginSession} from 'app/server/lib/ILoginSession'; import {ILoginSession} from 'app/server/lib/ILoginSession';
import * as log from 'app/server/lib/log'; import * as log from 'app/server/lib/log';
import {IMessage, Rpc} from 'grain-rpc'; import {IMessage, Rpc} from 'grain-rpc';
import {createCheckers} from 'ts-interface-checker'; import * as t from 'ts-interface-checker';
import {FlexServer} from './FlexServer'; import {FlexServer} from './FlexServer';
import {IInstanceManager} from './IInstanceManager'; import {IInstanceManager} from './IInstanceManager';
import {ITestingHooks} from './ITestingHooks'; import {ITestingHooks} from './ITestingHooks';
import ITestingHooksTI from './ITestingHooks-ti'; import ITestingHooksTI from './ITestingHooks-ti';
import {connect, fromCallback} from './serverUtils'; import {connect, fromCallback} from './serverUtils';
const tiCheckers = t.createCheckers(ITestingHooksTI, {UserProfile: t.name("object")});
export function startTestingHooks(socketPath: string, port: number, instanceManager: IInstanceManager, export function startTestingHooks(socketPath: string, port: number, instanceManager: IInstanceManager,
comm: Comm, flexServer: FlexServer, comm: Comm, flexServer: FlexServer,
workerServers: FlexServer[]): Promise<net.Server> { workerServers: FlexServer[]): Promise<net.Server> {
@ -27,7 +29,7 @@ export function startTestingHooks(socketPath: string, port: number, instanceMana
// Register the testing implementation. // Register the testing implementation.
rpc.registerImpl('testing', rpc.registerImpl('testing',
new TestingHooks(port, instanceManager, comm, flexServer, workerServers), new TestingHooks(port, instanceManager, comm, flexServer, workerServers),
createCheckers(ITestingHooksTI).ITestingHooks); tiCheckers.ITestingHooks);
}); });
server.listen(socketPath); server.listen(socketPath);
}); });
@ -47,7 +49,7 @@ export interface TestingHooksClient extends ITestingHooks {
export async function connectTestingHooks(socketPath: string): Promise<TestingHooksClient> { export async function connectTestingHooks(socketPath: string): Promise<TestingHooksClient> {
const socket = await connect(socketPath); const socket = await connect(socketPath);
const rpc = connectToSocket(new Rpc({logger: {}}), socket); const rpc = connectToSocket(new Rpc({logger: {}}), socket);
return Object.assign(rpc.getStub<TestingHooks>('testing', createCheckers(ITestingHooksTI).ITestingHooks), { return Object.assign(rpc.getStub<TestingHooks>('testing', tiCheckers.ITestingHooks), {
close: () => socket.end(), close: () => socket.end(),
}); });
} }
@ -61,12 +63,12 @@ export class TestingHooks implements ITestingHooks {
private _workerServers: FlexServer[] private _workerServers: FlexServer[]
) {} ) {}
public getOwnPort(): number { public async getOwnPort(): Promise<number> {
log.info("TestingHooks.getOwnPort called"); log.info("TestingHooks.getOwnPort called");
return this._server.getOwnPort(); return this._server.getOwnPort();
} }
public getPort(): number { public async getPort(): Promise<number> {
log.info("TestingHooks.getPort called"); log.info("TestingHooks.getPort called");
return this._port; return this._port;
} }
@ -100,7 +102,7 @@ export class TestingHooks implements ITestingHooks {
log.info("TestingHooks.setServerVersion called with", version); log.info("TestingHooks.setServerVersion called with", version);
this._comm.setServerVersion(version); this._comm.setServerVersion(version);
for (const server of this._workerServers) { for (const server of this._workerServers) {
await server.comm.setServerVersion(version); server.comm.setServerVersion(version);
} }
} }
@ -108,7 +110,7 @@ export class TestingHooks implements ITestingHooks {
log.info("TestingHooks.disconnectClients called"); log.info("TestingHooks.disconnectClients called");
this._comm.destroyAllClients(); this._comm.destroyAllClients();
for (const server of this._workerServers) { for (const server of this._workerServers) {
await server.comm.destroyAllClients(); server.comm.destroyAllClients();
} }
} }
@ -134,7 +136,7 @@ export class TestingHooks implements ITestingHooks {
log.info("TestingHooks.setClientPersistence called with", ttlMs); log.info("TestingHooks.setClientPersistence called with", ttlMs);
this._comm.testSetClientPersistence(ttlMs); this._comm.testSetClientPersistence(ttlMs);
for (const server of this._workerServers) { for (const server of this._workerServers) {
await server.comm.testSetClientPersistence(ttlMs); server.comm.testSetClientPersistence(ttlMs);
} }
} }
@ -174,7 +176,7 @@ export class TestingHooks implements ITestingHooks {
log.info("TestingHooks.flushAuthorizerCache called"); log.info("TestingHooks.flushAuthorizerCache called");
this._server.dbManager.flushDocAuthCache(); this._server.dbManager.flushDocAuthCache();
for (const server of this._workerServers) { for (const server of this._workerServers) {
await server.dbManager.flushDocAuthCache(); server.dbManager.flushDocAuthCache();
} }
} }

View File

@ -316,7 +316,7 @@ export async function createTmpDir(options: tmp.Options): Promise<TmpDirResult>
try { try {
// Still call the original callback, so that `tmp` module doesn't keep remembering about // Still call the original callback, so that `tmp` module doesn't keep remembering about
// this directory and doesn't try to delete it again on exit. // this directory and doesn't try to delete it again on exit.
tmpCleanup(); await tmpCleanup();
} catch (err) { } catch (err) {
// OK if it fails because the dir is already removed. // OK if it fails because the dir is already removed.
} }

View File

@ -21,7 +21,7 @@
}, },
"composite": true, "composite": true,
"plugins": [{ "plugins": [{
"name": "typescript-tslint-plugin" "name": "typescript-eslint-language-service"
}], }],
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,

View File

@ -4,7 +4,7 @@ export type ProductFlavor = string;
export interface CustomTheme { export interface CustomTheme {
bodyClassName?: string; bodyClassName?: string;
wideLogo?: boolean; wideLogo?: boolean;
}; }
export function getTheme(flavor: string): CustomTheme { export function getTheme(flavor: string): CustomTheme {
return { return {

View File

@ -15,7 +15,7 @@ import { decodeUrl } from 'app/common/gristUrls';
import { FullUser, UserProfile } from 'app/common/LoginSessionAPI'; import { FullUser, UserProfile } from 'app/common/LoginSessionAPI';
import { resetOrg } from 'app/common/resetOrg'; import { resetOrg } from 'app/common/resetOrg';
import { TestState } from 'app/common/TestState'; import { TestState } from 'app/common/TestState';
import { DocStateComparison, Organization as APIOrganization, UserAPIImpl, Workspace } from 'app/common/UserAPI'; import { Organization as APIOrganization, DocStateComparison, UserAPIImpl, Workspace } from 'app/common/UserAPI';
import { Organization } from 'app/gen-server/entity/Organization'; import { Organization } from 'app/gen-server/entity/Organization';
import { Product } from 'app/gen-server/entity/Product'; import { Product } from 'app/gen-server/entity/Product';
import { create } from 'app/server/lib/create'; import { create } from 'app/server/lib/create';
@ -163,7 +163,7 @@ export async function selectAll() {
* Returns a WebElementPromise for the .viewsection_content element for the section which contains * Returns a WebElementPromise for the .viewsection_content element for the section which contains
* the given RegExp content. * the given RegExp content.
*/ */
export function getSection(sectionOrTitle: string|WebElement): WebElement { export function getSection(sectionOrTitle: string|WebElement): WebElement|WebElementPromise {
if (typeof sectionOrTitle !== 'string') { return sectionOrTitle; } if (typeof sectionOrTitle !== 'string') { return sectionOrTitle; }
return driver.find(`.test-viewsection-title[value="${sectionOrTitle}" i]`) return driver.find(`.test-viewsection-title[value="${sectionOrTitle}" i]`)
.findClosest('.viewsection_content'); .findClosest('.viewsection_content');
@ -1187,7 +1187,7 @@ export class Session {
// Wipe the current site. The current user ends up being its only owner and manager. // Wipe the current site. The current user ends up being its only owner and manager.
public async resetSite() { public async resetSite() {
return resetOrg(await this.createHomeApi(), this.settings.org); return resetOrg(this.createHomeApi(), this.settings.org);
} }
// Return a session configured for the current session's site but a different user. // Return a session configured for the current session's site but a different user.

View File

@ -55,7 +55,7 @@ export class TestServerMerged implements IMochaServer {
public async restart(reset: boolean = false) { public async restart(reset: boolean = false) {
if (this.isExternalServer()) { return; } if (this.isExternalServer()) { return; }
if (this._starts > 0) { if (this._starts > 0) {
await this.resume(); this.resume();
await this.stop(); await this.stop();
} }
this._starts++; this._starts++;