mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +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