(core) add initial support for special shares

Summary:
This gives a mechanism for controlling access control within a document that is distinct from (though implemented with the same machinery as) granular access rules.

It was hard to find a good way to insert this that didn't dissolve in a soup of complications, so here's what I went with:
 * When reading rules, if there are shares, extra rules are added.
 * If there are shares, all rules are made conditional on a "ShareRef" user property.
 * "ShareRef" is null when a doc is accessed in normal way, and the row id of a share when accessed via a share.

There's no UI for controlling shares (George is working on it for forms), but you can do it by editing a `_grist_Shares` table in a document. Suppose you make a fresh document with a single page/table/widget, then to create an empty share you can do:

```
gristDocPageModel.gristDoc.get().docData.sendAction(['AddRecord', '_grist_Shares', null, {linkId: 'xyz', options: '{"publish": true}'}])
```

If you look at the home db now there should be something in the `shares` table:

```
$ sqlite3 -table landing.db "select * from shares"
+----+------------------------+------------------------+--------------+---------+
| id |          key           |         doc_id         |   link_id    | options |
+----+------------------------+------------------------+--------------+---------+
| 1  | gSL4g38PsyautLHnjmXh2K | 4qYuace1xP2CTcPunFdtan | xyz | ...      |
+----+------------------------+------------------------+--------------+---------+
```

If you take the key from that (gSL4g38PsyautLHnjmXh2K in this case) and replace the document's urlId in its URL with `s.<key>` (in this case `s.gSL4g38PsyautLHnjmXh2K` then you can use the regular document landing page (it will be quite blank initially) or API endpoint via the share.

E.g. for me `http://localhost:8080/o/docs/s0gSL4g38PsyautLHnjmXh2K/share-inter-3` accesses the doc.

To actually share some material - useful commands:

```
gristDocPageModel.gristDoc.get().docData.getMetaTable('_grist_Views_section').getRecords()
gristDocPageModel.gristDoc.get().docData.sendAction(['UpdateRecord', '_grist_Views_section', 1, {shareOptions: '{"publish": true, "form": true}'}])
gristDocPageModel.gristDoc.get().docData.getMetaTable('_grist_Pages').getRecords()
gristDocPageModel.gristDoc.get().docData.sendAction(['UpdateRecord', '_grist_Pages', 1, {shareRef: 1}])
```

For a share to be effective, at least one page needs to have its shareRef set to the rowId of the share, and at least one widget on one of those pages needs to have its shareOptions set to {"publish": "true", "form": "true"} (meaning turn on sharing, and include form sharing), and the share itself needs {"publish": true} on its options.

I think special shares are kind of incompatible with public sharing, since by their nature (allowing access to all endpoints) they easily expose the docId, and changing that would be hard.

Test Plan: tests added

Reviewers: dsagal, georgegevoian

Reviewed By: dsagal, georgegevoian

Subscribers: jarek, dsagal

Differential Revision: https://phab.getgrist.com/D4144
This commit is contained in:
Paul Fitzpatrick
2024-01-03 11:53:20 -05:00
parent f079d4b340
commit 2a206dfcf8
39 changed files with 897 additions and 68 deletions

View File

@@ -1832,6 +1832,22 @@ export class ActiveDoc extends EventEmitter {
));
}
public async syncShares(docSession: OptDocSession) {
const metaTables = await this.fetchMetaTables(docSession);
const shares = metaTables['_grist_Shares'];
const ids = shares[2];
const vals = shares[3];
const goodShares = ids.map((id, idx) => {
return {
id,
linkId: String(vals['linkId'][idx]),
options: String(vals['options'][idx]),
};
});
await this.getHomeDbManager()?.syncShares(this.docName, goodShares);
return goodShares;
}
/**
* Loads an open document from DocStorage. Returns a list of the tables it contains.
*/

View File

@@ -6,7 +6,7 @@
import * as express from 'express';
import {ApiError} from 'app/common/ApiError';
import {getSlugIfNeeded, parseUrlId} from 'app/common/gristUrls';
import {getSlugIfNeeded, parseUrlId, SHARE_KEY_PREFIX} from 'app/common/gristUrls';
import {LocalPlugin} from "app/common/plugin";
import {TELEMETRY_TEMPLATE_SIGNUP_COOKIE_NAME} from 'app/common/Telemetry';
import {Document as APIDocument} from 'app/common/UserAPI';
@@ -105,7 +105,8 @@ export function attachAppEndpoint(options: AttachOptions): void {
const slug = getSlugIfNeeded(doc);
const slugMismatch = (req.params.slug || null) !== (slug || null);
const preferredUrlId = doc.urlId || doc.id;
if (urlId !== preferredUrlId || slugMismatch) {
if (!req.params.viaShare && // Don't bother canonicalizing for shares yet.
(urlId !== preferredUrlId || slugMismatch)) {
// Prepare to redirect to canonical url for document.
// Preserve any query parameters or fragments.
const queryOrFragmentCheck = req.originalUrl.match(/([#?].*)/);
@@ -215,6 +216,14 @@ export function attachAppEndpoint(options: AttachOptions): void {
// The * is a wildcard in express 4, rather than a regex symbol.
// See https://expressjs.com/en/guide/routing.html
app.get('/doc/:urlId([^/]+):remainder(*)', ...docMiddleware, docHandler);
app.get('/s/:urlId([^/]+):remainder(*)',
(req, res, next) => {
// /s/<key> is another way of writing /doc/<prefix><key> for shares.
req.params.urlId = SHARE_KEY_PREFIX + req.params.urlId;
req.params.viaShare = "1";
next();
},
...docMiddleware, docHandler);
app.get('/:urlId([^-/]{12,})(/:slug([^/]+):remainder(*))?',
...docMiddleware, docHandler);
}

View File

@@ -13,7 +13,7 @@ import {
} from 'app/common/DocActions';
import {isRaisedException} from "app/common/gristTypes";
import {Box, RenderBox, RenderContext} from "app/common/Forms";
import {buildUrlId, parseUrlId} from "app/common/gristUrls";
import {buildUrlId, parseUrlId, SHARE_KEY_PREFIX} from "app/common/gristUrls";
import {isAffirmative, safeJsonParse, timeoutReached} from "app/common/gutil";
import {SchemaTypes} from "app/common/schema";
import {SortFunc} from 'app/common/SortFunc';
@@ -178,6 +178,12 @@ export class DocWorkerApi {
* to apply to these routes.
*/
public addEndpoints() {
this._app.use((req, res, next) => {
if (req.url.startsWith('/api/s/')) {
req.url = req.url.replace('/api/s/', `/api/docs/${SHARE_KEY_PREFIX}`);
}
next();
});
// check document exists (not soft deleted) and user can view it
const canView = expressWrap(this._assertAccess.bind(this, 'viewers', false));

View File

@@ -8,8 +8,9 @@ import * as path from 'path';
import {ApiError} from 'app/common/ApiError';
import {mapSetOrClear, MapWithTTL} from 'app/common/AsyncCreate';
import {BrowserSettings} from 'app/common/BrowserSettings';
import {DocCreationInfo, DocEntry, DocListAPI, OpenDocMode, OpenLocalDocResult} from 'app/common/DocListAPI';
import {DocCreationInfo, DocEntry, DocListAPI, OpenDocOptions, OpenLocalDocResult} from 'app/common/DocListAPI';
import {FilteredDocUsageSummary} from 'app/common/DocUsage';
import {parseUrlId} from 'app/common/gristUrls';
import {Invite} from 'app/common/sharing';
import {tbind} from 'app/common/tbind';
import {TelemetryMetadataByLevel} from 'app/common/Telemetry';
@@ -298,8 +299,13 @@ export class DocManager extends EventEmitter {
* `doc` - the object with metadata tables.
*/
public async openDoc(client: Client, docId: string,
openMode: OpenDocMode = 'default',
linkParameters: Record<string, string> = {}): Promise<OpenLocalDocResult> {
options?: OpenDocOptions): Promise<OpenLocalDocResult> {
if (typeof options === 'string') {
throw new Error('openDoc call with outdated parameter type');
}
const openMode = options?.openMode || 'default';
const linkParameters = options?.linkParameters || {};
const originalUrlId = options?.originalUrlId;
let auth: Authorizer;
let userId: number | undefined;
const dbManager = this._homeDbManager;
@@ -313,7 +319,12 @@ export class DocManager extends EventEmitter {
// We use docId in the key, and disallow urlId, so we can be sure that we are looking at the
// right doc when we re-query the DB over the life of the websocket.
const key = {urlId: docId, userId, org};
const useShareUrlId = Boolean(originalUrlId && parseUrlId(originalUrlId).shareKey);
const key = {
urlId: useShareUrlId ? originalUrlId! : docId,
userId,
org
};
log.debug("DocManager.openDoc Authorizer key", key);
const docAuth = await dbManager.getDocAuthCached(key);
assertAccess('viewers', docAuth);

View File

@@ -166,6 +166,16 @@ export function getDocSessionAccess(docSession: OptDocSession): Role {
throw new Error('getDocSessionAccess could not find access information in DocSession');
}
export function getDocSessionShare(docSession: OptDocSession): string|null {
if (docSession.authorizer) {
return docSession.authorizer.getCachedAuth().cachedDoc?.linkId || null;
}
if (docSession.req) {
return docSession.req.docAuth?.cachedDoc?.linkId || null;
}
return null;
}
export function getDocSessionAccessOrNull(docSession: OptDocSession): Role|null {
try {
return getDocSessionAccess(docSession);

View File

@@ -37,8 +37,8 @@ import { HomeDBManager } from 'app/gen-server/lib/HomeDBManager';
import { GristObjCode } from 'app/plugin/GristData';
import { compileAclFormula } from 'app/server/lib/ACLFormula';
import { DocClients } from 'app/server/lib/DocClients';
import { getDocSessionAccess, getDocSessionAltSessionId, getDocSessionUser,
OptDocSession } from 'app/server/lib/DocSession';
import { getDocSessionAccess, getDocSessionAltSessionId, getDocSessionShare,
getDocSessionUser, OptDocSession } from 'app/server/lib/DocSession';
import { DocStorage, REMOVE_UNUSED_ATTACHMENTS_DELAY } from 'app/server/lib/DocStorage';
import log from 'app/server/lib/log';
import { IPermissionInfo, MixedPermissionSetWithContext,
@@ -98,7 +98,8 @@ function isAddOrUpdateRecordAction([actionName]: UserAction): boolean {
// specifically _grist_Attachments.
const STRUCTURAL_TABLES = new Set(['_grist_Tables', '_grist_Tables_column', '_grist_Views',
'_grist_Views_section', '_grist_Views_section_field',
'_grist_ACLResources', '_grist_ACLRules']);
'_grist_ACLResources', '_grist_ACLRules',
'_grist_Shares']);
// Actions that won't be allowed (yet) for a user with nuanced access to a document.
// A few may be innocuous, but that hasn't been figured out yet.
@@ -238,7 +239,8 @@ export interface GranularAccessForBundle {
* (the UserAction has been compiled to DocActions).
* - canApplyBundle(), called when DocActions have been produced from UserActions,
* but before those DocActions have been applied to the DB. If fails, the modification
* will be abandoned.
* will be abandoned. This method will also finalize some bundle state,
* specifically the `maybeHasShareChanges` flag.
* - appliedBundle(), called when DocActions have been applied to the DB, but before
* those changes have been sent to clients.
* - sendDocUpdateForBundle() is called once a bundle has been applied, to notify
@@ -279,7 +281,9 @@ export class GranularAccess implements GranularAccessForBundle {
// Flag for whether doc actions mention a rule change, even if passive due to
// schema changes.
hasAnyRuleChange: boolean,
maybeHasShareChanges: boolean,
options: ApplyUAExtendedOptions|null,
shareRef?: number;
}|null;
public constructor(
@@ -308,6 +312,7 @@ export class GranularAccess implements GranularAccessForBundle {
this._activeBundle = {
docSession, docActions, undo, userActions, isDirect,
applied: false, hasDeliberateRuleChange: false, hasAnyRuleChange: false,
maybeHasShareChanges: false,
options,
};
this._activeBundle.hasDeliberateRuleChange =
@@ -497,6 +502,35 @@ export class GranularAccess implements GranularAccessForBundle {
return this._checkIncomingDocAction({docSession, action, actionIdx});
}
}));
const shares = this._docData.getMetaTable('_grist_Shares');
/**
* This is a good point at which to determine whether we may be
* making a change to special shares. If we may be, then currently
* we will reload any connected web clients accessing the document
* via a share.
*
* The role of the `maybeHasShareChanges` flag is to trigger
* reloads of web clients that are accessing the document via a
* share, if share configuration may have changed. It doesn't
* actually impact access control itself. The sketch of order of
* operations given in the docstring for the GranularAccess
* class is helpful for understanding this flow.
*
* At the time of writing, web client support for special shares
* is not an official feature - but it is super convenient for testing
* and will be important later.
*/
if (shares.getRowIds().length > 0 &&
docActions.some(
action => getTableId(action).startsWith('_grist'))) {
// TODO: could actually compare new rules with old rules and
// see if they've changed. Or could exclude some tables that
// could easily change without an impact on share rules,
// such as _grist_Attachments. Either improvement could
// greatly reduce unnecessary web client reloads for shares
// if that becomes an issue.
this._activeBundle.maybeHasShareChanges = true;
}
}
await this._canApplyCellActions(currentUser, userIsOwner);
@@ -517,6 +551,8 @@ export class GranularAccess implements GranularAccessForBundle {
_grist_Tables_column: this._docData.getMetaTable('_grist_Tables_column').getTableDataAction(),
_grist_ACLResources: this._docData.getMetaTable('_grist_ACLResources').getTableDataAction(),
_grist_ACLRules: this._docData.getMetaTable('_grist_ACLRules').getTableDataAction(),
_grist_Shares: this._docData.getMetaTable('_grist_Shares').getTableDataAction(),
// WATCH OUT - Shares may need more tables, check.
});
for (const da of docActions) {
tmpDocData.receiveAction(da);
@@ -534,6 +570,8 @@ export class GranularAccess implements GranularAccessForBundle {
throw new ApiError(err.message, 400);
}
}
// TODO: any changes needed to this logic for shares?
}
/**
@@ -593,6 +631,11 @@ export class GranularAccess implements GranularAccessForBundle {
throw new ErrorWithCode('NEED_RELOAD', 'document needs reload, access rules changed');
}
const linkId = getDocSessionShare(docSession);
if (linkId && this._activeBundle?.maybeHasShareChanges) {
throw new ErrorWithCode('NEED_RELOAD', 'document needs reload, share may have changed');
}
// Optimize case where there are no rules to enforce.
if (!this._ruler.haveRules()) { return docActions; }
@@ -1354,7 +1397,15 @@ export class GranularAccess implements GranularAccessForBundle {
await this.update();
return;
}
if (!this._ruler.haveRules()) { return; }
const shares = this._docData.getMetaTable('_grist_Shares');
if (shares.getRowIds().length > 0 &&
docActions.some(action => getTableId(action).startsWith('_grist'))) {
await this.update();
return;
}
if (!shares && !this._ruler.haveRules()) {
return;
}
// If there is a schema change, redo from scratch for now.
if (docActions.some(docAction => isSchemaAction(docAction))) {
await this.update();
@@ -1799,6 +1850,20 @@ export class GranularAccess implements GranularAccessForBundle {
const attrs = this._getUserAttributes(docSession);
access = getDocSessionAccess(docSession);
const linkId = getDocSessionShare(docSession);
let shareRef: number = 0;
if (linkId) {
const rowIds = this._docData.getMetaTable('_grist_Shares').filterRowIds({
linkId,
});
if (rowIds.length > 1) {
throw new Error('Share identifier is not unique');
}
if (rowIds.length === 1) {
shareRef = rowIds[0];
}
}
if (docSession.forkingAsOwner) {
// For granular access purposes, we become an owner.
// It is a bit of a bluff, done on the understanding that this session will
@@ -1823,6 +1888,7 @@ export class GranularAccess implements GranularAccessForBundle {
}
const user = new User();
user.Access = access;
user.ShareRef = shareRef || null;
const isAnonymous = fullUser?.id === this._homeDbManager?.getAnonymousUserId() ||
fullUser?.id === null;
user.UserID = (!isAnonymous && fullUser?.id) || null;
@@ -2569,7 +2635,7 @@ export class Ruler {
* Update granular access from DocData.
*/
public async update(docData: DocData) {
await this.ruleCollection.update(docData, {log, compile: compileAclFormula, includeHelperCols: true});
await this.ruleCollection.update(docData, {log, compile: compileAclFormula, enrichRulesForImplementation: true});
// Also clear the per-docSession cache of rule evaluations.
this.clearCache();
@@ -3039,6 +3105,8 @@ function getCensorMethod(tableId: string): (rec: RecordEditor) => void {
return rec => rec;
case '_grist_ACLRules':
return rec => rec;
case '_grist_Shares':
return rec => rec;
case '_grist_Cells':
return rec => rec.set('content', [GristObjCode.Censored]).set('userRef', '');
default:
@@ -3174,6 +3242,7 @@ export class User implements UserInfo {
public Email: string | null = null;
public SessionID: string | null = null;
public UserRef: string | null = null;
public ShareRef: number | null = null;
[attribute: string]: any;
constructor(_info: Record<string, unknown> = {}) {

View File

@@ -338,6 +338,14 @@ export class Sharing {
const actionSummary = await this._activeDoc.handleTriggers(localActionBundle);
// Opportunistically use actionSummary to see if _grist_Shares was
// changed.
if (actionSummary.tableDeltas._grist_Shares) {
// This is a little risky, since it entangles us with home db
// availability. But we aren't doing a lot...?
await this._activeDoc.syncShares(makeExceptionalDocSession('system'));
}
await this._activeDoc.updateRowCount(sandboxActionBundle.rowCount, docSession);
// Broadcast the action to connected browsers.

View File

@@ -6,7 +6,7 @@ export const GRIST_DOC_SQL = `
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT '');
INSERT INTO _grist_DocInfo VALUES(1,'','','',40,'','');
INSERT INTO _grist_DocInfo VALUES(1,'','','',41,'','');
CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0, "recordCardViewSectionRef" INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "description" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL);
CREATE TABLE IF NOT EXISTS "_grist_Imports" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "origFileName" TEXT DEFAULT '', "parseFormula" TEXT DEFAULT '', "delimiter" TEXT DEFAULT '', "doublequote" BOOLEAN DEFAULT 0, "escapechar" TEXT DEFAULT '', "quotechar" TEXT DEFAULT '', "skipinitialspace" BOOLEAN DEFAULT 0, "encoding" TEXT DEFAULT '', "hasHeaders" BOOLEAN DEFAULT 0);
@@ -15,9 +15,9 @@ CREATE TABLE IF NOT EXISTS "_grist_External_table" (id INTEGER PRIMARY KEY, "tab
CREATE TABLE IF NOT EXISTS "_grist_TableViews" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "viewRef" INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS "_grist_TabItems" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "viewRef" INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS "_grist_TabBar" (id INTEGER PRIMARY KEY, "viewRef" INTEGER DEFAULT 0, "tabPos" NUMERIC DEFAULT 1e999);
CREATE TABLE IF NOT EXISTS "_grist_Pages" (id INTEGER PRIMARY KEY, "viewRef" INTEGER DEFAULT 0, "indentation" INTEGER DEFAULT 0, "pagePos" NUMERIC DEFAULT 1e999);
CREATE TABLE IF NOT EXISTS "_grist_Pages" (id INTEGER PRIMARY KEY, "viewRef" INTEGER DEFAULT 0, "indentation" INTEGER DEFAULT 0, "pagePos" NUMERIC DEFAULT 1e999, "shareRef" INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS "_grist_Views" (id INTEGER PRIMARY KEY, "name" TEXT DEFAULT '', "type" TEXT DEFAULT '', "layoutSpec" TEXT DEFAULT '');
CREATE TABLE IF NOT EXISTS "_grist_Views_section" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "parentId" INTEGER DEFAULT 0, "parentKey" TEXT DEFAULT '', "title" TEXT DEFAULT '', "description" TEXT DEFAULT '', "defaultWidth" INTEGER DEFAULT 0, "borderWidth" INTEGER DEFAULT 0, "theme" TEXT DEFAULT '', "options" TEXT DEFAULT '', "chartType" TEXT DEFAULT '', "layoutSpec" TEXT DEFAULT '', "filterSpec" TEXT DEFAULT '', "sortColRefs" TEXT DEFAULT '', "linkSrcSectionRef" INTEGER DEFAULT 0, "linkSrcColRef" INTEGER DEFAULT 0, "linkTargetColRef" INTEGER DEFAULT 0, "embedId" TEXT DEFAULT '', "rules" TEXT DEFAULT NULL);
CREATE TABLE IF NOT EXISTS "_grist_Views_section" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "parentId" INTEGER DEFAULT 0, "parentKey" TEXT DEFAULT '', "title" TEXT DEFAULT '', "description" TEXT DEFAULT '', "defaultWidth" INTEGER DEFAULT 0, "borderWidth" INTEGER DEFAULT 0, "theme" TEXT DEFAULT '', "options" TEXT DEFAULT '', "chartType" TEXT DEFAULT '', "layoutSpec" TEXT DEFAULT '', "filterSpec" TEXT DEFAULT '', "sortColRefs" TEXT DEFAULT '', "linkSrcSectionRef" INTEGER DEFAULT 0, "linkSrcColRef" INTEGER DEFAULT 0, "linkTargetColRef" INTEGER DEFAULT 0, "embedId" TEXT DEFAULT '', "rules" TEXT DEFAULT NULL, "shareOptions" TEXT DEFAULT '');
CREATE TABLE IF NOT EXISTS "_grist_Views_section_field" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colRef" INTEGER DEFAULT 0, "width" INTEGER DEFAULT 0, "widgetOptions" TEXT DEFAULT '', "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "filter" TEXT DEFAULT '', "rules" TEXT DEFAULT NULL);
CREATE TABLE IF NOT EXISTS "_grist_Validations" (id INTEGER PRIMARY KEY, "formula" TEXT DEFAULT '', "name" TEXT DEFAULT '', "tableRef" INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS "_grist_REPL_Hist" (id INTEGER PRIMARY KEY, "code" TEXT DEFAULT '', "outputText" TEXT DEFAULT '', "errorText" TEXT DEFAULT '');
@@ -35,6 +35,7 @@ INSERT INTO _grist_ACLPrincipals VALUES(4,'group','','','Viewers','');
CREATE TABLE IF NOT EXISTS "_grist_ACLMemberships" (id INTEGER PRIMARY KEY, "parent" INTEGER DEFAULT 0, "child" INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS "_grist_Filters" (id INTEGER PRIMARY KEY, "viewSectionRef" INTEGER DEFAULT 0, "colRef" INTEGER DEFAULT 0, "filter" TEXT DEFAULT '', "pinned" BOOLEAN DEFAULT 0);
CREATE TABLE IF NOT EXISTS "_grist_Cells" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "colRef" INTEGER DEFAULT 0, "rowId" INTEGER DEFAULT 0, "root" BOOLEAN DEFAULT 0, "parentId" INTEGER DEFAULT 0, "type" INTEGER DEFAULT 0, "content" TEXT DEFAULT '', "userRef" TEXT DEFAULT '');
CREATE TABLE IF NOT EXISTS "_grist_Shares" (id INTEGER PRIMARY KEY, "linkId" TEXT DEFAULT '', "options" TEXT DEFAULT '', "label" TEXT DEFAULT '', "description" TEXT DEFAULT '');
CREATE INDEX _grist_Attachments_fileIdent ON _grist_Attachments(fileIdent);
COMMIT;
`;
@@ -43,7 +44,7 @@ export const GRIST_DOC_WITH_TABLE1_SQL = `
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT '');
INSERT INTO _grist_DocInfo VALUES(1,'','','',40,'','');
INSERT INTO _grist_DocInfo VALUES(1,'','','',41,'','');
CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0, "recordCardViewSectionRef" INTEGER DEFAULT 0);
INSERT INTO _grist_Tables VALUES(1,'Table1',1,0,0,2,3);
CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "description" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL);
@@ -58,14 +59,14 @@ CREATE TABLE IF NOT EXISTS "_grist_TableViews" (id INTEGER PRIMARY KEY, "tableRe
CREATE TABLE IF NOT EXISTS "_grist_TabItems" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "viewRef" INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS "_grist_TabBar" (id INTEGER PRIMARY KEY, "viewRef" INTEGER DEFAULT 0, "tabPos" NUMERIC DEFAULT 1e999);
INSERT INTO _grist_TabBar VALUES(1,1,1);
CREATE TABLE IF NOT EXISTS "_grist_Pages" (id INTEGER PRIMARY KEY, "viewRef" INTEGER DEFAULT 0, "indentation" INTEGER DEFAULT 0, "pagePos" NUMERIC DEFAULT 1e999);
INSERT INTO _grist_Pages VALUES(1,1,0,1);
CREATE TABLE IF NOT EXISTS "_grist_Pages" (id INTEGER PRIMARY KEY, "viewRef" INTEGER DEFAULT 0, "indentation" INTEGER DEFAULT 0, "pagePos" NUMERIC DEFAULT 1e999, "shareRef" INTEGER DEFAULT 0);
INSERT INTO _grist_Pages VALUES(1,1,0,1,0);
CREATE TABLE IF NOT EXISTS "_grist_Views" (id INTEGER PRIMARY KEY, "name" TEXT DEFAULT '', "type" TEXT DEFAULT '', "layoutSpec" TEXT DEFAULT '');
INSERT INTO _grist_Views VALUES(1,'Table1','raw_data','');
CREATE TABLE IF NOT EXISTS "_grist_Views_section" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "parentId" INTEGER DEFAULT 0, "parentKey" TEXT DEFAULT '', "title" TEXT DEFAULT '', "description" TEXT DEFAULT '', "defaultWidth" INTEGER DEFAULT 0, "borderWidth" INTEGER DEFAULT 0, "theme" TEXT DEFAULT '', "options" TEXT DEFAULT '', "chartType" TEXT DEFAULT '', "layoutSpec" TEXT DEFAULT '', "filterSpec" TEXT DEFAULT '', "sortColRefs" TEXT DEFAULT '', "linkSrcSectionRef" INTEGER DEFAULT 0, "linkSrcColRef" INTEGER DEFAULT 0, "linkTargetColRef" INTEGER DEFAULT 0, "embedId" TEXT DEFAULT '', "rules" TEXT DEFAULT NULL);
INSERT INTO _grist_Views_section VALUES(1,1,1,'record','','',100,1,'','','','','','[]',0,0,0,'',NULL);
INSERT INTO _grist_Views_section VALUES(2,1,0,'record','','',100,1,'','','','','','',0,0,0,'',NULL);
INSERT INTO _grist_Views_section VALUES(3,1,0,'single','','',100,1,'','','','','','',0,0,0,'',NULL);
CREATE TABLE IF NOT EXISTS "_grist_Views_section" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "parentId" INTEGER DEFAULT 0, "parentKey" TEXT DEFAULT '', "title" TEXT DEFAULT '', "description" TEXT DEFAULT '', "defaultWidth" INTEGER DEFAULT 0, "borderWidth" INTEGER DEFAULT 0, "theme" TEXT DEFAULT '', "options" TEXT DEFAULT '', "chartType" TEXT DEFAULT '', "layoutSpec" TEXT DEFAULT '', "filterSpec" TEXT DEFAULT '', "sortColRefs" TEXT DEFAULT '', "linkSrcSectionRef" INTEGER DEFAULT 0, "linkSrcColRef" INTEGER DEFAULT 0, "linkTargetColRef" INTEGER DEFAULT 0, "embedId" TEXT DEFAULT '', "rules" TEXT DEFAULT NULL, "shareOptions" TEXT DEFAULT '');
INSERT INTO _grist_Views_section VALUES(1,1,1,'record','','',100,1,'','','','','','[]',0,0,0,'',NULL,'');
INSERT INTO _grist_Views_section VALUES(2,1,0,'record','','',100,1,'','','','','','',0,0,0,'',NULL,'');
INSERT INTO _grist_Views_section VALUES(3,1,0,'single','','',100,1,'','','','','','',0,0,0,'',NULL,'');
CREATE TABLE IF NOT EXISTS "_grist_Views_section_field" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colRef" INTEGER DEFAULT 0, "width" INTEGER DEFAULT 0, "widgetOptions" TEXT DEFAULT '', "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "filter" TEXT DEFAULT '', "rules" TEXT DEFAULT NULL);
INSERT INTO _grist_Views_section_field VALUES(1,1,1,2,0,'',0,0,'',NULL);
INSERT INTO _grist_Views_section_field VALUES(2,1,2,3,0,'',0,0,'',NULL);
@@ -92,6 +93,7 @@ INSERT INTO _grist_ACLPrincipals VALUES(4,'group','','','Viewers','');
CREATE TABLE IF NOT EXISTS "_grist_ACLMemberships" (id INTEGER PRIMARY KEY, "parent" INTEGER DEFAULT 0, "child" INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS "_grist_Filters" (id INTEGER PRIMARY KEY, "viewSectionRef" INTEGER DEFAULT 0, "colRef" INTEGER DEFAULT 0, "filter" TEXT DEFAULT '', "pinned" BOOLEAN DEFAULT 0);
CREATE TABLE IF NOT EXISTS "_grist_Cells" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "colRef" INTEGER DEFAULT 0, "rowId" INTEGER DEFAULT 0, "root" BOOLEAN DEFAULT 0, "parentId" INTEGER DEFAULT 0, "type" INTEGER DEFAULT 0, "content" TEXT DEFAULT '', "userRef" TEXT DEFAULT '');
CREATE TABLE IF NOT EXISTS "_grist_Shares" (id INTEGER PRIMARY KEY, "linkId" TEXT DEFAULT '', "options" TEXT DEFAULT '', "label" TEXT DEFAULT '', "description" TEXT DEFAULT '');
CREATE TABLE IF NOT EXISTS "Table1" (id INTEGER PRIMARY KEY, "manualSort" NUMERIC DEFAULT 1e999, "A" BLOB DEFAULT NULL, "B" BLOB DEFAULT NULL, "C" BLOB DEFAULT NULL);
CREATE INDEX _grist_Attachments_fileIdent ON _grist_Attachments(fileIdent);
COMMIT;