mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) move home server into core
Summary: This moves enough server material into core to run a home server. The data engine is not yet incorporated (though in manual testing it works when ported). Test Plan: existing tests pass Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2552
This commit is contained in:
164
app/server/lib/TimeQuery.ts
Normal file
164
app/server/lib/TimeQuery.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import {ActionSummary, ColumnDelta, createEmptyActionSummary, createEmptyTableDelta} from 'app/common/ActionSummary';
|
||||
import {CellDelta} from 'app/common/TabularDiff';
|
||||
import {concatenateSummaries} from 'app/server/lib/ActionSummary';
|
||||
import {ISQLiteDB, quoteIdent, ResultRow} from 'app/server/lib/SQLiteDB';
|
||||
import keyBy = require('lodash/keyBy');
|
||||
import matches = require('lodash/matches');
|
||||
import sortBy = require('lodash/sortBy');
|
||||
import toPairs = require('lodash/toPairs');
|
||||
|
||||
/**
|
||||
* We can combine an ActionSummary with the current state of the database
|
||||
* to answer questions about the state of the database in the past. This
|
||||
* is particularly useful for grist metadata tables, which are needed to
|
||||
* interpret the content of user tables fully.
|
||||
* - TimeCursor is a simple container for the db and an ActionSummary
|
||||
* - TimeQuery offers a db-like interface for a given table and set of columns
|
||||
* - TimeLayout answers a couple of concrete questions about table meta-data using a
|
||||
* set of TimeQuery objects hooked up to _grist_* tables. It could be used to
|
||||
* improve the rendering of the ActionLog, for example, although it is not (yet).
|
||||
*/
|
||||
|
||||
/** Track the state of the database at a particular time. */
|
||||
export class TimeCursor {
|
||||
public summary: ActionSummary;
|
||||
|
||||
constructor(public db: ISQLiteDB) {
|
||||
this.summary = createEmptyActionSummary();
|
||||
}
|
||||
|
||||
/** add a summary of an action just before the last action applied to the TimeCursor */
|
||||
public prepend(prevSummary: ActionSummary) {
|
||||
this.summary = concatenateSummaries([prevSummary, this.summary]);
|
||||
}
|
||||
|
||||
/** add a summary of an action just after the last action applied to the TimeCursor */
|
||||
public append(nextSummary: ActionSummary) {
|
||||
this.summary = concatenateSummaries([this.summary, nextSummary]);
|
||||
}
|
||||
}
|
||||
|
||||
/** internal class for storing a ResultRow dictionary, keyed by rowId */
|
||||
class ResultRows {
|
||||
[rowId: number]: ResultRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the state of a particular table in the past, given a TimeCursor holding the
|
||||
* current db and a summary of all changes between that past time and now.
|
||||
* For the moment, for simplicity, names of tables and columns are assumed not to
|
||||
* change, and TimeQuery should only be used for _grist_* tables.
|
||||
*/
|
||||
export class TimeQuery {
|
||||
private _currentRows: ResultRow[];
|
||||
private _pastRows: ResultRow[];
|
||||
|
||||
constructor(public tc: TimeCursor, public tableId: string, public colIds: string[]) {
|
||||
}
|
||||
|
||||
/** Get fresh data from DB and overlay with any past data */
|
||||
public async update(): Promise<ResultRow[]> {
|
||||
this._currentRows = await this.tc.db.all(
|
||||
`select ${['id', ...this.colIds].map(quoteIdent).join(',')} from ${quoteIdent(this.tableId)}`);
|
||||
|
||||
// Let's see everything the summary has accumulated about the table back then.
|
||||
const td = this.tc.summary.tableDeltas[this.tableId] || createEmptyTableDelta();
|
||||
|
||||
// Now rewrite the summary as a ResultRow dictionary, to make it comparable
|
||||
// with database.
|
||||
const summaryRows: ResultRows = {};
|
||||
for (const [colId, columns] of toPairs(td.columnDeltas)) {
|
||||
for (const [rowId, cell] of toPairs(columns) as unknown as Array<[keyof ColumnDelta, CellDelta]>) {
|
||||
if (!summaryRows[rowId]) { summaryRows[rowId] = {}; }
|
||||
const val = cell[0];
|
||||
summaryRows[rowId][colId] = (val !== null && typeof val === 'object' ) ? val[0] : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare to access the current database state by rowId.
|
||||
const rowsById = keyBy(this._currentRows, r => (r.id as number));
|
||||
|
||||
// Prepare a list of rowIds at the time of interest.
|
||||
// The past rows are whatever the db has now, omitting rows that were added
|
||||
// since the past time, and adding back any rows that were removed since then.
|
||||
// Careful about the order of this, since rows could be replaced.
|
||||
const additions = new Set(td.addRows);
|
||||
const pastRowIds =
|
||||
new Set([...this._currentRows.map(r => r.id as number).filter(r => !additions.has(r)),
|
||||
...td.removeRows]);
|
||||
|
||||
// Now prepare a row for every expected rowId, using current db data if available
|
||||
// and relevant, and overlaying past data when available.
|
||||
this._pastRows = new Array<ResultRow>();
|
||||
const colIdsOfInterest = new Set(this.colIds);
|
||||
for (const id of Array.from(pastRowIds).sort()) {
|
||||
const row: ResultRow = rowsById[id] || {id};
|
||||
if (summaryRows[id] && !additions.has(id)) {
|
||||
for (const [colId, val] of toPairs(summaryRows[id])) {
|
||||
if (colIdsOfInterest.has(colId)) { row[colId] = val; }
|
||||
}
|
||||
}
|
||||
this._pastRows.push(row);
|
||||
}
|
||||
return this._pastRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a query with a single result, specifying any desired filters. Exception thrown
|
||||
* if there is no result.
|
||||
*/
|
||||
public one(args: {[name: string]: any}): ResultRow {
|
||||
const result = this._pastRows.find(matches(args));
|
||||
if (!result) {
|
||||
throw new Error(`could not find: ${JSON.stringify(args)} for ${this.tableId}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Get all results for a query. */
|
||||
public all(args?: {[name: string]: any}): ResultRow[] {
|
||||
if (!args) { return this._pastRows; }
|
||||
return this._pastRows.filter(matches(args));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put some TimeQuery queries to work answering questions about column order and
|
||||
* user-facing name of tables.
|
||||
*/
|
||||
export class TimeLayout {
|
||||
public tables: TimeQuery;
|
||||
public fields: TimeQuery;
|
||||
public columns: TimeQuery;
|
||||
public views: TimeQuery;
|
||||
|
||||
constructor(public tc: TimeCursor) {
|
||||
this.tables = new TimeQuery(tc, '_grist_Tables', ['tableId', 'primaryViewId']);
|
||||
this.fields = new TimeQuery(tc, '_grist_Views_section_field',
|
||||
['parentId', 'parentPos', 'colRef']);
|
||||
this.columns = new TimeQuery(tc, '_grist_Tables_column', ['parentId', 'colId']);
|
||||
this.views = new TimeQuery(tc, '_grist_Views', ['id', 'name']);
|
||||
}
|
||||
|
||||
/** update from TimeCursor */
|
||||
public async update() {
|
||||
await this.tables.update();
|
||||
await this.columns.update();
|
||||
await this.fields.update();
|
||||
await this.views.update();
|
||||
}
|
||||
|
||||
public getColumnOrder(tableId: string): string[] {
|
||||
const primaryViewId = this.tables.one({tableId}).primaryViewId;
|
||||
const preorder = this.fields.all({parentId: primaryViewId});
|
||||
const precol = keyBy(this.columns.all(), 'id');
|
||||
const ordered = sortBy(preorder, 'parentPos');
|
||||
const names = ordered.map(r => precol[r.colRef].colId);
|
||||
return names;
|
||||
}
|
||||
|
||||
public getTableName(tableId: string): string {
|
||||
const primaryViewId = this.tables.one({tableId}).primaryViewId;
|
||||
return this.views.one({id: primaryViewId}).name;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user