mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Persist forks in home db
Summary: Adds information about forks to the home db. This will be used later by the UI to list forks of documents. Test Plan: Browser and server tests. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3772
This commit is contained in:
@@ -78,6 +78,7 @@ import {DocReplacementOptions, DocState, DocStateComparison} from 'app/common/Us
|
||||
import {convertFromColumn} from 'app/common/ValueConverter';
|
||||
import {guessColInfoWithDocData} from 'app/common/ValueGuesser';
|
||||
import {parseUserAction} from 'app/common/ValueParser';
|
||||
import {Document} from 'app/gen-server/entity/Document';
|
||||
import {ParseOptions} from 'app/plugin/FileParserAPI';
|
||||
import {AccessTokenOptions, AccessTokenResult, GristDocAPI} from 'app/plugin/GristAPI';
|
||||
import {compileAclFormula} from 'app/server/lib/ACLFormula';
|
||||
@@ -1375,10 +1376,16 @@ export class ActiveDoc extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fork the current document. In fact, all that requires is calculating a good
|
||||
* ID for the fork. TODO: reconcile the two ways there are now of preparing a fork.
|
||||
* Fork the current document.
|
||||
*
|
||||
* TODO: reconcile the two ways there are now of preparing a fork.
|
||||
*/
|
||||
public async fork(docSession: OptDocSession): Promise<ForkResult> {
|
||||
const dbManager = this.getHomeDbManager();
|
||||
if (!dbManager) {
|
||||
throw new Error('HomeDbManager not available');
|
||||
}
|
||||
|
||||
const user = getDocSessionUser(docSession);
|
||||
// For now, fork only if user can read everything (or is owner).
|
||||
// TODO: allow forks with partial content.
|
||||
@@ -1387,9 +1394,19 @@ export class ActiveDoc extends EventEmitter {
|
||||
}
|
||||
const userId = user.id;
|
||||
const isAnonymous = this._docManager.isAnonymous(userId);
|
||||
|
||||
// Get fresh document metadata (the cached metadata doesn't include the urlId).
|
||||
const doc = await docSession.authorizer?.getDoc();
|
||||
if (!doc) { throw new Error('document id not known'); }
|
||||
let doc: Document | undefined;
|
||||
if (docSession.authorizer) {
|
||||
doc = await docSession.authorizer.getDoc();
|
||||
} else if (docSession.req) {
|
||||
doc = await this.getHomeDbManager()?.getDoc(docSession.req);
|
||||
}
|
||||
if (!doc) { throw new Error('Document not found'); }
|
||||
|
||||
// Don't allow creating forks of forks (for now).
|
||||
if (doc.trunkId) { throw new ApiError("Cannot fork a document that's already a fork", 400); }
|
||||
|
||||
const trunkDocId = doc.id;
|
||||
const trunkUrlId = doc.urlId || doc.id;
|
||||
await this.flushDoc(); // Make sure fork won't be too out of date.
|
||||
@@ -1415,6 +1432,8 @@ export class ActiveDoc extends EventEmitter {
|
||||
if (resp.status !== 200) {
|
||||
throw new ApiError(resp.statusText, resp.status);
|
||||
}
|
||||
|
||||
await dbManager.forkDoc(userId, doc, forkIds.forkId);
|
||||
} finally {
|
||||
await permitStore.removePermit(permitKey);
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ import {ApiError} from 'app/common/ApiError';
|
||||
import {BrowserSettings} from "app/common/BrowserSettings";
|
||||
import {BulkColValues, ColValues, fromTableDataAction, TableColValues, TableRecordValue} from 'app/common/DocActions';
|
||||
import {isRaisedException} from "app/common/gristTypes";
|
||||
import {parseUrlId} from "app/common/gristUrls";
|
||||
import {buildUrlId, parseUrlId} from "app/common/gristUrls";
|
||||
import {isAffirmative} from "app/common/gutil";
|
||||
import {SortFunc} from 'app/common/SortFunc';
|
||||
import {Sort} from 'app/common/SortSpec';
|
||||
import {MetaRowRecord} from 'app/common/TableData';
|
||||
import {DocReplacementOptions, DocState, DocStateComparison, DocStates, NEW_DOCUMENT_CODE} from 'app/common/UserAPI';
|
||||
import {HomeDBManager, makeDocAuthResult} from 'app/gen-server/lib/HomeDBManager';
|
||||
import {HomeDBManager, makeDocAuthResult, QueryResult} from 'app/gen-server/lib/HomeDBManager';
|
||||
import * as Types from "app/plugin/DocApiTypes";
|
||||
import DocApiTypesTI from "app/plugin/DocApiTypes-ti";
|
||||
import GristDataTI from 'app/plugin/GristData-ti';
|
||||
@@ -1181,12 +1181,26 @@ export class DocWorkerApi {
|
||||
const scope = getDocScope(req);
|
||||
const docId = getDocId(req);
|
||||
if (permanent) {
|
||||
// Soft delete the doc first, to de-list the document.
|
||||
await this._dbManager.softDeleteDocument(scope);
|
||||
// Delete document content from storage.
|
||||
await this._docManager.deleteDoc(null, docId, true);
|
||||
const {forkId} = parseUrlId(docId);
|
||||
if (!forkId) {
|
||||
// Soft delete the doc first, to de-list the document.
|
||||
await this._dbManager.softDeleteDocument(scope);
|
||||
}
|
||||
// Delete document content from storage. Include forks if doc is a trunk.
|
||||
const forks = forkId ? [] : await this._dbManager.getDocForks(docId);
|
||||
const docsToDelete = [
|
||||
docId,
|
||||
...forks.map((fork) =>
|
||||
buildUrlId({forkId: fork.id, forkUserId: fork.createdBy!, trunkId: docId})),
|
||||
];
|
||||
await Promise.all(docsToDelete.map(docName => this._docManager.deleteDoc(null, docName, true)));
|
||||
// Permanently delete from database.
|
||||
const query = await this._dbManager.deleteDocument(scope);
|
||||
let query: QueryResult<number>;
|
||||
if (forkId) {
|
||||
query = await this._dbManager.deleteFork({...scope, urlId: forkId});
|
||||
} else {
|
||||
query = await this._dbManager.deleteDocument(scope);
|
||||
}
|
||||
this._dbManager.checkQueryResult(query);
|
||||
await sendReply(req, res, query);
|
||||
} else {
|
||||
|
||||
@@ -556,9 +556,14 @@ export class HostedStorageManager implements IDocStorageManager {
|
||||
* This is called when a document was edited by the user.
|
||||
*/
|
||||
private _markAsEdited(docName: string, timestamp: string): void {
|
||||
if (parseUrlId(docName).snapshotId || !this._metadataManager) { return; }
|
||||
if (!this._metadataManager) { return; }
|
||||
|
||||
const {forkId, snapshotId} = parseUrlId(docName);
|
||||
if (snapshotId) { return; }
|
||||
|
||||
// Schedule a metadata update for the modified doc.
|
||||
this._metadataManager.scheduleUpdate(docName, {updatedAt: timestamp});
|
||||
const docId = forkId || docName;
|
||||
this._metadataManager.scheduleUpdate(docId, {updatedAt: timestamp});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,6 +30,7 @@ export function makeForkIds(options: { userId: number|null, isAnonymous: boolean
|
||||
const docId = parseUrlId(options.trunkDocId).trunkId;
|
||||
const urlId = parseUrlId(options.trunkUrlId).trunkId;
|
||||
return {
|
||||
forkId,
|
||||
docId: buildUrlId({trunkId: docId, forkId, forkUserId}),
|
||||
urlId: buildUrlId({trunkId: urlId, forkId, forkUserId}),
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ export const TEST_HTTPS_OFFSET = process.env.GRIST_TEST_HTTPS_OFFSET ?
|
||||
const INTERNAL_FIELDS = new Set([
|
||||
'apiKey', 'billingAccountId', 'firstLoginAt', 'filteredOut', 'ownerId', 'gracePeriodStart', 'stripeCustomerId',
|
||||
'stripeSubscriptionId', 'stripePlanId', 'stripeProductId', 'userId', 'isFirstTimeUser', 'allowGoogleLogin',
|
||||
'authSubject', 'usage'
|
||||
'authSubject', 'usage', 'createdBy'
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user