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 {BillingAPI, BillingAPIImpl} from 'app/common/BillingAPI';
|
||||||
import {BrowserSettings} from 'app/common/BrowserSettings';
|
import {BrowserSettings} from 'app/common/BrowserSettings';
|
||||||
import {BulkColValues, TableColValues, UserAction} from 'app/common/DocActions';
|
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 {Features} from 'app/common/Features';
|
||||||
import {isClient} from 'app/common/gristUrls';
|
import {isClient} from 'app/common/gristUrls';
|
||||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||||
@ -104,11 +104,22 @@ export interface Workspace extends WorkspaceProperties {
|
|||||||
isSupportWorkspace?: boolean;
|
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 {
|
export interface DocumentProperties extends CommonProperties {
|
||||||
isPinned: boolean;
|
isPinned: boolean;
|
||||||
urlId: string|null;
|
urlId: string|null;
|
||||||
|
options: DocumentOptions|null;
|
||||||
}
|
}
|
||||||
export const documentPropertyKeys = [...commonPropertyKeys, 'isPinned', 'urlId'];
|
|
||||||
|
export const documentPropertyKeys = [...commonPropertyKeys, 'isPinned', 'urlId', 'options'];
|
||||||
|
|
||||||
export interface Document extends DocumentProperties {
|
export interface Document extends DocumentProperties {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {ApiError} from 'app/common/ApiError';
|
import {ApiError} from 'app/common/ApiError';
|
||||||
import {Role} from 'app/common/roles';
|
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 {nativeValues} from 'app/gen-server/lib/values';
|
||||||
import {Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryColumn} from "typeorm";
|
import {Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryColumn} from "typeorm";
|
||||||
import {AclRuleDoc} from "./AclRule";
|
import {AclRuleDoc} from "./AclRule";
|
||||||
@ -55,6 +55,9 @@ export class Document extends Resource {
|
|||||||
@OneToMany(type => Alias, alias => alias.doc)
|
@OneToMany(type => Alias, alias => alias.doc)
|
||||||
public aliases: Alias[];
|
public aliases: Alias[];
|
||||||
|
|
||||||
|
@Column({name: 'options', type: nativeValues.jsonEntityType, nullable: true})
|
||||||
|
public options: DocumentOptions | null;
|
||||||
|
|
||||||
public checkProperties(props: any): props is Partial<DocumentProperties> {
|
public checkProperties(props: any): props is Partial<DocumentProperties> {
|
||||||
return super.checkProperties(props, documentPropertyKeys);
|
return super.checkProperties(props, documentPropertyKeys);
|
||||||
}
|
}
|
||||||
@ -68,5 +71,45 @@ export class Document extends Resource {
|
|||||||
}
|
}
|
||||||
this.urlId = props.urlId;
|
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
|
// 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.
|
// situations where the user is working with non-deleted resources.
|
||||||
if (key === 'removedAt' && value === null) { return undefined; }
|
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 INTERNAL_FIELDS.has(key) ? undefined : value;
|
||||||
});
|
});
|
||||||
return JSON.parse(output);
|
return JSON.parse(output);
|
||||||
|
Loading…
Reference in New Issue
Block a user