Skip to content

Commit

Permalink
Move common interfaces between host and iframe to protocol file with …
Browse files Browse the repository at this point in the history
…JSON schema validation
  • Loading branch information
mattmazzola committed Jun 15, 2016
1 parent 7ff0561 commit a68ecbf
Show file tree
Hide file tree
Showing 9 changed files with 1,090 additions and 795 deletions.
1,514 changes: 808 additions & 706 deletions e2e/protocol.e2e.spec.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"jquery": "^2.2.3",
"jshint": "^2.9.1",
"jshint-stylish": "^2.1.0",
"json-loader": "^0.5.4",
"karma": "^0.13.19",
"karma-chrome-launcher": "^0.2.2",
"karma-coverage": "^0.5.3",
Expand All @@ -57,6 +58,7 @@
},
"dependencies": {
"http-post-message": "file:///C:\\Users\\mattm\\Source\\Repos\\http-post-message",
"jsen": "^0.6.1",
"powerbi-filters": "file:///C:\\Users\\mattm\\Source\\Repos\\powerbi-filters",
"powerbi-router": "file:///C:\\Users\\mattm\\Source\\Repos\\powerbi-router",
"window-post-message-proxy": "file:///C:\\Users\\mattm\\Source\\Repos\\window-post-message-proxy"
Expand Down
9 changes: 5 additions & 4 deletions src/core.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Embed, IEmbedConstructor, IEmbedOptions, IHpmFactory, IWpmpFactory, IRouterFactory } from './embed';
import { Embed, IEmbedConstructor, IHpmFactory, IWpmpFactory, IRouterFactory } from './embed';
import * as protocol from './protocol';
import { Report } from './report';
import { Tile } from './tile';
import { Utils } from './util';
Expand Down Expand Up @@ -94,7 +95,7 @@ export class PowerBi {
* If component has already been created and attached to element re-use component instance and existing iframe,
* otherwise create a new component instance
*/
embed(element: HTMLElement, config: IEmbedOptions = {}): Embed {
embed(element: HTMLElement, config: protocol.IEmbedOptions = {}): Embed {
let component: Embed;
let powerBiElement = <IPowerBiElement>element;

Expand All @@ -112,7 +113,7 @@ export class PowerBi {
* Given an html element embed component base configuration.
* Save component instance on element for later lookup.
*/
private embedNew(element: IPowerBiElement, config: IEmbedOptions): Embed {
private embedNew(element: IPowerBiElement, config: protocol.IEmbedOptions): Embed {
const componentType = config.type || element.getAttribute(Embed.typeAttribute);
if (!componentType) {
throw new Error(`Attempted to embed using config ${JSON.stringify(config)} on element ${element.outerHTML}, but could not determine what type of component to embed. You must specify a type in the configuration or as an attribute such as '${Embed.typeAttribute}="${Report.type.toLowerCase()}"'.`);
Expand All @@ -138,7 +139,7 @@ export class PowerBi {
return component;
}

private embedExisting(element: IPowerBiElement, config: IEmbedOptions): Embed {
private embedExisting(element: IPowerBiElement, config: protocol.IEmbedOptions): Embed {
const component = Utils.find(x => x.element === element, this.embeds);
if (!component) {
throw new Error(`Attempted to embed using config ${JSON.stringify(config)} on element ${element.outerHTML} which already has embedded comopnent associated, but could not find the existing comopnent in the list of active components. This could indicate the embeds list is out of sync with the DOM, or the component is referencing the incorrect HTML element.`);
Expand Down
38 changes: 8 additions & 30 deletions src/embed.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Utils } from './util';
import * as protocol from './protocol';
import * as report from './report';
import * as wpmp from 'window-post-message-proxy';
import * as hpm from 'http-post-message';
Expand All @@ -23,33 +24,10 @@ declare global {
}
}

/**
* TODO: Consider adding type: "report" | "tile" property to indicate what type of object to embed
*
* This would align with goal of having single embed page which adapts to the thing being embedded
* instead of having M x N embed pages where M is type of object (report, tile) and N is authorization
* type (PaaS, SaaS, Anonymous)
*/
export interface ILoadMessage {
accessToken: string;
id: string;
}

export interface IEmbedOptions {
type?: string;
id?: string;
accessToken?: string;
embedUrl?: string;
webUrl?: string;
name?: string;
filterPaneEnabled?: boolean;
getGlobalAccessToken?: () => string;
logMessages?: boolean;
wpmpName?: string;
}

export interface IEmbedConstructor {
new(hpmFactory: IHpmFactory, wpmpFactory: IWpmpFactory, routerFactory: IRouterFactory, element: HTMLElement, options: IEmbedOptions): Embed;
new(hpmFactory: IHpmFactory, wpmpFactory: IWpmpFactory, routerFactory: IRouterFactory, element: HTMLElement, options: protocol.IEmbedOptions): Embed;
}

export interface IHpmFactory {
Expand Down Expand Up @@ -77,7 +55,7 @@ export abstract class Embed {
/**
* Default options for embeddable component.
*/
private static defaultOptions: IEmbedOptions = {
private static defaultOptions: protocol.IEmbedOptions = {
filterPaneEnabled: true,
logMessages: false,
wpmpName: 'SdkReportWpmp'
Expand All @@ -88,9 +66,9 @@ export abstract class Embed {
router: router.Router;
element: HTMLElement;
iframe: HTMLIFrameElement;
options: IEmbedOptions;
options: protocol.IEmbedOptions;

constructor(hpmFactory: IHpmFactory, wpmpFactory: IWpmpFactory, routerFactory: IRouterFactory, element: HTMLElement, options: IEmbedOptions) {
constructor(hpmFactory: IHpmFactory, wpmpFactory: IWpmpFactory, routerFactory: IRouterFactory, element: HTMLElement, options: protocol.IEmbedOptions) {
this.element = element;

// TODO: Change when Object.assign is available.
Expand All @@ -114,18 +92,18 @@ export abstract class Embed {
* This is used to inject configuration options such as access token, loadAction, etc
* which allow iframe to load the actual report with authentication.
*/
load(options: IEmbedOptions, requireId: boolean = false, message: ILoadMessage = null): Promise<void> {
load(options: protocol.IEmbedOptions, requireId: boolean = false, message: protocol.ILoad = null): Promise<void> {
if(!message) {
throw new Error(`You called load without providing message properties from the concrete embeddable class.`);
}

const baseMessage = <ILoadMessage>{
const baseMessage = <protocol.ILoad>{
accessToken: options.accessToken
};

Utils.assign(message, baseMessage);

return this.hpm.post<report.IError[]>('/report/load', message)
return this.hpm.post<protocol.IError[]>('/report/load', message)
.catch(response => {
throw response.body;
});
Expand Down
240 changes: 240 additions & 0 deletions src/protocol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import * as jsen from 'jsen';

interface IValidationError {
path: string;
keyword: string;
message: string;
}

export interface IError {
message: string;
}

function normalizeError(error: IValidationError): IError {
if(!error.message) {
error.message = `${error.path} is invalid. Not meeting ${error.keyword} constraint`;
}

delete error.path;
delete error.keyword;

return error;
}

/**
* Takes in schema and returns function which can be used to validate the schema with better semantics around exposing errors
*/
export function validate(schema: any, options?: any) {
return (x: any): any[] => {
const validate = jsen(schema, options);
const isValid = validate(x);

if(isValid) {
return undefined;
}
else {
return validate.errors
.map(normalizeError);
}
}
}


export interface ISettings {
filter?: any;
filterPaneEnabled?: boolean;
pageName?: string;
pageNavigationEnabled?: boolean;
}

export const settingsSchema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"filter": {
"type": "object"
},
"filterPaneEnabled": {
"type": "boolean",
"messages": {
"type": "filterPaneEnabled must be a boolean"
}
},
"pageName": {
"type": "string",
"messages": {
"type": "pageName must be a string"
}
},
"pageNavigationEnabled": {
"type": "boolean",
"messages": {
"type": "pageNavigationEnabled must be a boolean"
}
}
}
};

export const validateSettings = validate(settingsSchema);

/**
* TODO: Consider adding type: "report" | "tile" property to indicate what type of object to embed
*
* This would align with goal of having single embed page which adapts to the thing being embedded
* instead of having M x N embed pages where M is type of object (report, tile) and N is authorization
* type (PaaS, SaaS, Anonymous)
*/
export interface ILoad {
accessToken: string;
id: string;
settings?: ISettings;
}

export const loadSchema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"accessToken": {
"type": "string",
"messages": {
"type": "accessToken must be a string",
"required": "accessToken is required"
},
"invalidMessage": "accessToken property is invalid"
},
"id": {
"type": "string",
"messages": {
"type": "id must be a string",
"required": "id is required"
}
},
"settings": {
"$ref": "#settings"
}
},
"required": [
"accessToken",
"id"
]
};

export const validateLoad = validate(loadSchema, {
schemas: {
settings: settingsSchema
}
});

export interface IEmbedOptions {
type?: string;
id?: string;
accessToken?: string;
embedUrl?: string;
filterPaneEnabled?: boolean;
getGlobalAccessToken?: () => string;
logMessages?: boolean;
wpmpName?: string;
}

export interface IReportEmbedOptions extends IEmbedOptions {
settings: ISettings;
}

export interface IPageTarget {
type: "page";
name: string;
}
export const pageTargetSchema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"page"
],
"messages": {
"type": "type must be a string",
"enum": "type must be 'page'",
"required": "type is required"
}
},
"name": {
"type": "string",
"messages": {
"type": "name must be a string",
"required": "name is required"
}
}
},
"required": [
"type",
"name"
]
};
export const validatePageTarget = validate(pageTargetSchema);

export interface IVisualTarget {
type: "visual";
id: string;
}
export const visualTargetSchema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"visual"
],
"messages": {
"type": "type must be a string",
"enum": "type must be 'visual'",
"required": "type is required"
}
},
"id": {
"type": "string",
"messages": {
"type": "id must be a string",
"required": "id is required"
}
}
},
"required": [
"type",
"id"
]
};
export const validateVisualTarget = validate(visualTargetSchema);

export interface IPage {
name: string;
displayName: string;
}
export const pageSchema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"name": {
"type": "string",
"messages": {
"type": "name must be a string",
"required": "name is required"
}
},
"displayName": {
"type": "string",
"messages": {
"type": "displayName must be a string",
"required": "displayName is required"
}
}
},
"required": [
"name",
"displayName"
]
};

export const validatePage = validate(pageSchema);
Loading

0 comments on commit a68ecbf

Please sign in to comment.