mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) add docs.options column to home db to store doc description, icon, openMode
Summary: Bundles some new document options into a JSON column. The icon option is treated somewhat gingerly. It is intended, at least initially, to store an image thumbnail for a document as a url to hand-prepared assets (for examples and templates), so it is locked down to a particular url prefix to avoid opening the door to mischief. Test Plan: added test Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D2916
This commit is contained in:
parent
e5eeb3ec80
commit
997be24a21
@ -4,7 +4,7 @@ import {BaseAPI, IOptions} from 'app/common/BaseAPI';
|
||||
import {BillingAPI, BillingAPIImpl} from 'app/common/BillingAPI';
|
||||
import {BrowserSettings} from 'app/common/BrowserSettings';
|
||||
import {BulkColValues, TableColValues, UserAction} from 'app/common/DocActions';
|
||||
import {DocCreationInfo} from 'app/common/DocListAPI';
|
||||
import {DocCreationInfo, OpenDocMode} from 'app/common/DocListAPI';
|
||||
import {Features} from 'app/common/Features';
|
||||
import {isClient} from 'app/common/gristUrls';
|
||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||
@ -104,11 +104,22 @@ export interface Workspace extends WorkspaceProperties {
|
||||
isSupportWorkspace?: boolean;
|
||||
}
|
||||
|
||||
// Non-core options for a document.
|
||||
// "Non-core" means bundled into a single options column in the database.
|
||||
// TODO: consider smoothing over this distinction in the API.
|
||||
export interface DocumentOptions {
|
||||
description?: string|null;
|
||||
icon?: string|null;
|
||||
openMode?: OpenDocMode|null;
|
||||
}
|
||||
|
||||
export interface DocumentProperties extends CommonProperties {
|
||||
isPinned: boolean;
|
||||
urlId: string|null;
|
||||
options: DocumentOptions|null;
|
||||
}
|
||||
export const documentPropertyKeys = [...commonPropertyKeys, 'isPinned', 'urlId'];
|
||||
|
||||
export const documentPropertyKeys = [...commonPropertyKeys, 'isPinned', 'urlId', 'options'];
|
||||
|
||||
export interface Document extends DocumentProperties {
|
||||
id: string;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {ApiError} from 'app/common/ApiError';
|
||||
import {Role} from 'app/common/roles';
|
||||
import {DocumentProperties, documentPropertyKeys, NEW_DOCUMENT_CODE} from "app/common/UserAPI";
|
||||
import {DocumentOptions, DocumentProperties, documentPropertyKeys, NEW_DOCUMENT_CODE} from "app/common/UserAPI";
|
||||
import {nativeValues} from 'app/gen-server/lib/values';
|
||||
import {Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryColumn} from "typeorm";
|
||||
import {AclRuleDoc} from "./AclRule";
|
||||
@ -55,6 +55,9 @@ export class Document extends Resource {
|
||||
@OneToMany(type => Alias, alias => alias.doc)
|
||||
public aliases: Alias[];
|
||||
|
||||
@Column({name: 'options', type: nativeValues.jsonEntityType, nullable: true})
|
||||
public options: DocumentOptions | null;
|
||||
|
||||
public checkProperties(props: any): props is Partial<DocumentProperties> {
|
||||
return super.checkProperties(props, documentPropertyKeys);
|
||||
}
|
||||
@ -68,5 +71,45 @@ export class Document extends Resource {
|
||||
}
|
||||
this.urlId = props.urlId;
|
||||
}
|
||||
if (props.options !== undefined) {
|
||||
// Options are merged over the existing state - unless options
|
||||
// object is set to "null", in which case the state is wiped
|
||||
// completely.
|
||||
if (props.options === null) {
|
||||
this.options = null;
|
||||
} else {
|
||||
this.options = this.options || {};
|
||||
if (props.options.description !== undefined) {
|
||||
this.options.description = props.options.description;
|
||||
}
|
||||
if (props.options.openMode !== undefined) {
|
||||
this.options.openMode = props.options.openMode;
|
||||
}
|
||||
if (props.options.icon !== undefined) {
|
||||
this.options.icon = sanitizeIcon(props.options.icon);
|
||||
}
|
||||
// Normalize so that null equates with absence.
|
||||
for (const key of Object.keys(this.options) as Array<keyof DocumentOptions>) {
|
||||
if (this.options[key] === null) {
|
||||
delete this.options[key];
|
||||
}
|
||||
}
|
||||
// Normalize so that no options set equates with absense.
|
||||
if (Object.keys(this.options).length === 0) {
|
||||
this.options = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check that icon points to an expected location. This will definitely
|
||||
// need changing, it is just a placeholder as the icon feature is developed.
|
||||
function sanitizeIcon(icon: string|null) {
|
||||
if (icon === null) { return icon; }
|
||||
const url = new URL(icon);
|
||||
if (url.protocol !== 'https:' || url.host !== 'grist-static.com' || !url.pathname.startsWith('/icons/')) {
|
||||
throw new ApiError('invalid document icon', 400);
|
||||
}
|
||||
return url.href;
|
||||
}
|
||||
|
17
app/gen-server/migration/1626369037484-DocOptions.ts
Normal file
17
app/gen-server/migration/1626369037484-DocOptions.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { nativeValues } from 'app/gen-server/lib/values';
|
||||
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
|
||||
|
||||
export class DocOptions1626369037484 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.addColumn('docs', new TableColumn({
|
||||
name: 'options',
|
||||
type: nativeValues.jsonType,
|
||||
isNullable: true,
|
||||
}));
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.dropColumn('docs', 'options');
|
||||
}
|
||||
}
|
@ -193,6 +193,8 @@ export function pruneAPIResult<T>(data: T): T {
|
||||
// Do not include removedAt field if it is not set. It is not relevant to regular
|
||||
// situations where the user is working with non-deleted resources.
|
||||
if (key === 'removedAt' && value === null) { return undefined; }
|
||||
// Don't bother sending option fields if there are no options set.
|
||||
if (key === 'options' && value === null) { return undefined; }
|
||||
return INTERNAL_FIELDS.has(key) ? undefined : value;
|
||||
});
|
||||
return JSON.parse(output);
|
||||
|
Loading…
Reference in New Issue
Block a user