Load plugin data even when hosted at non-root (#679)

Summary:
This commit approximately completes the implementation of #643.\* Plugin
adapters are now provided an `Assets` object at `load` time, which they
can use to resolve their plugin-specific API routes.

\* “Approximately” because there are some non-essential pieces of legacy
code that should be cleaned up.

Test Plan:
Unit tests modified, but it would be good to also manually test this.
Run `./scripts/build_static_site.sh` to build the site to, say,
`/tmp/gateway/`. Then spin up a static HTTP server serving `/tmp/` and
navigate to `/gateway/` in the browser. Note that the entire application
works.

wchargin-branch: use-assets-in-PluginAdapters
This commit is contained in:
William Chargin 2018-08-15 17:21:51 -07:00 committed by GitHub
parent b252a6b5de
commit 5ac2494586
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 74 additions and 25 deletions

View File

@ -3,4 +3,5 @@
## [Unreleased] ## [Unreleased]
- Start tracking changes in `CHANGELOG.md` - Start tracking changes in `CHANGELOG.md`
- Aggregate over connection types in the cred explorer (#502) - Aggregate over connection types in the cred explorer (#502)
- Support hosting SourceCred instances at arbitrary gateways, not just the root of a domain (#643)

View File

@ -2,6 +2,7 @@
import {Graph, type NodeAddressT, type EdgeAddressT} from "../../core/graph"; import {Graph, type NodeAddressT, type EdgeAddressT} from "../../core/graph";
import {NodeTrie, EdgeTrie} from "../../core/trie"; import {NodeTrie, EdgeTrie} from "../../core/trie";
import type {Assets} from "../assets";
import type {Repo} from "../../core/repo"; import type {Repo} from "../../core/repo";
import type { import type {
@ -68,8 +69,8 @@ export class StaticAdapterSet {
return this._typeEdgeTrie.getLast(x); return this._typeEdgeTrie.getLast(x);
} }
load(repo: Repo): Promise<DynamicAdapterSet> { load(assets: Assets, repo: Repo): Promise<DynamicAdapterSet> {
return Promise.all(this._adapters.map((a) => a.load(repo))).then( return Promise.all(this._adapters.map((a) => a.load(assets, repo))).then(
(adapters) => new DynamicAdapterSet(this, adapters) (adapters) => new DynamicAdapterSet(this, adapters)
); );
} }

View File

@ -9,6 +9,7 @@ import {
import type {DynamicPluginAdapter} from "./pluginAdapter"; import type {DynamicPluginAdapter} from "./pluginAdapter";
import {StaticAdapterSet} from "./adapterSet"; import {StaticAdapterSet} from "./adapterSet";
import {FallbackStaticAdapter, FALLBACK_NAME} from "./fallbackAdapter"; import {FallbackStaticAdapter, FALLBACK_NAME} from "./fallbackAdapter";
import {Assets} from "../assets";
import {makeRepo, type Repo} from "../../core/repo"; import {makeRepo, type Repo} from "../../core/repo";
describe("app/adapters/adapterSet", () => { describe("app/adapters/adapterSet", () => {
@ -57,8 +58,10 @@ describe("app/adapters/adapterSet", () => {
]; ];
} }
load(_unused_repo: Repo) { load(assets: Assets, repo: Repo) {
return this.loadingMock().then(() => new TestDynamicPluginAdapter()); return this.loadingMock(assets, repo).then(
() => new TestDynamicPluginAdapter()
);
} }
} }
@ -159,7 +162,14 @@ describe("app/adapters/adapterSet", () => {
it("loads a dynamicAdapterSet", async () => { it("loads a dynamicAdapterSet", async () => {
const {x, sas} = example(); const {x, sas} = example();
x.loadingMock.mockResolvedValue(); x.loadingMock.mockResolvedValue();
const das = await sas.load(makeRepo("foo", "bar")); expect(x.loadingMock).toHaveBeenCalledTimes(0);
const assets = new Assets("/my/gateway/");
const repo = makeRepo("foo", "bar");
const das = await sas.load(assets, repo);
expect(x.loadingMock).toHaveBeenCalledTimes(1);
expect(x.loadingMock.mock.calls[0]).toHaveLength(2);
expect(x.loadingMock.mock.calls[0][0]).toBe(assets);
expect(x.loadingMock.mock.calls[0][1]).toBe(repo);
expect(das).toEqual(expect.anything()); expect(das).toEqual(expect.anything());
}); });
}); });
@ -169,7 +179,10 @@ describe("app/adapters/adapterSet", () => {
const x = new TestStaticPluginAdapter(); const x = new TestStaticPluginAdapter();
const sas = new StaticAdapterSet([x]); const sas = new StaticAdapterSet([x]);
x.loadingMock.mockResolvedValue(); x.loadingMock.mockResolvedValue();
const das = await sas.load(makeRepo("foo", "bar")); const das = await sas.load(
new Assets("/my/gateway/"),
makeRepo("foo", "bar")
);
return {x, sas, das}; return {x, sas, das};
} }
it("allows retrieval of the original StaticAdapterSet", async () => { it("allows retrieval of the original StaticAdapterSet", async () => {

View File

@ -6,6 +6,7 @@ import {
type NodeAddressT, type NodeAddressT,
EdgeAddress, EdgeAddress,
} from "../../core/graph"; } from "../../core/graph";
import type {Assets} from "../assets";
import type {Repo} from "../../core/repo"; import type {Repo} from "../../core/repo";
import type {StaticPluginAdapter, DynamicPluginAdapter} from "./pluginAdapter"; import type {StaticPluginAdapter, DynamicPluginAdapter} from "./pluginAdapter";
@ -46,7 +47,7 @@ export class FallbackStaticAdapter implements StaticPluginAdapter {
]; ];
} }
load(_unused_repo: Repo) { load(_unused_assets: Assets, _unused_repo: Repo) {
return Promise.resolve(new FallbackDynamicAdapter()); return Promise.resolve(new FallbackDynamicAdapter());
} }
} }

View File

@ -1,6 +1,7 @@
// @flow // @flow
import {Graph, type NodeAddressT, type EdgeAddressT} from "../../core/graph"; import {Graph, type NodeAddressT, type EdgeAddressT} from "../../core/graph";
import type {Assets} from "../assets";
import type {Repo} from "../../core/repo"; import type {Repo} from "../../core/repo";
export type EdgeType = {| export type EdgeType = {|
@ -22,7 +23,7 @@ export interface StaticPluginAdapter {
edgePrefix(): EdgeAddressT; edgePrefix(): EdgeAddressT;
nodeTypes(): NodeType[]; nodeTypes(): NodeType[];
edgeTypes(): EdgeType[]; edgeTypes(): EdgeType[];
load(repo: Repo): Promise<DynamicPluginAdapter>; load(assets: Assets, repo: Repo): Promise<DynamicPluginAdapter>;
} }
export interface DynamicPluginAdapter { export interface DynamicPluginAdapter {

View File

@ -91,7 +91,9 @@ export function createApp(
</div> </div>
<button <button
disabled={subType !== "READY_TO_LOAD_GRAPH"} disabled={subType !== "READY_TO_LOAD_GRAPH"}
onClick={() => this.stateTransitionMachine.loadGraph()} onClick={() =>
this.stateTransitionMachine.loadGraph(this.props.assets)
}
> >
Load graph Load graph
</button> </button>

View File

@ -4,6 +4,7 @@ import deepEqual from "lodash.isequal";
import * as NullUtil from "../../util/null"; import * as NullUtil from "../../util/null";
import {Graph} from "../../core/graph"; import {Graph} from "../../core/graph";
import type {Assets} from "../../app/assets";
import type {Repo} from "../../core/repo"; import type {Repo} from "../../core/repo";
import {type EdgeEvaluator} from "../../core/attribution/pagerank"; import {type EdgeEvaluator} from "../../core/attribution/pagerank";
import { import {
@ -78,7 +79,7 @@ export function initialState(): AppState {
export interface StateTransitionMachineInterface { export interface StateTransitionMachineInterface {
+setRepo: (Repo) => void; +setRepo: (Repo) => void;
+setEdgeEvaluator: (EdgeEvaluator) => void; +setEdgeEvaluator: (EdgeEvaluator) => void;
+loadGraph: () => Promise<void>; +loadGraph: (Assets) => Promise<void>;
+runPagerank: () => Promise<void>; +runPagerank: () => Promise<void>;
} }
/* In production, instantiate via createStateTransitionMachine; the constructor /* In production, instantiate via createStateTransitionMachine; the constructor
@ -88,7 +89,10 @@ export interface StateTransitionMachineInterface {
export class StateTransitionMachine implements StateTransitionMachineInterface { export class StateTransitionMachine implements StateTransitionMachineInterface {
getState: () => AppState; getState: () => AppState;
setState: (AppState) => void; setState: (AppState) => void;
loadGraphWithAdapters: (repo: Repo) => Promise<GraphWithAdapters>; loadGraphWithAdapters: (
assets: Assets,
repo: Repo
) => Promise<GraphWithAdapters>;
pagerank: ( pagerank: (
Graph, Graph,
EdgeEvaluator, EdgeEvaluator,
@ -98,7 +102,10 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
constructor( constructor(
getState: () => AppState, getState: () => AppState,
setState: (AppState) => void, setState: (AppState) => void,
loadGraphWithAdapters: (repo: Repo) => Promise<GraphWithAdapters>, loadGraphWithAdapters: (
assets: Assets,
repo: Repo
) => Promise<GraphWithAdapters>,
pagerank: ( pagerank: (
Graph, Graph,
EdgeEvaluator, EdgeEvaluator,
@ -160,7 +167,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
} }
} }
async loadGraph() { async loadGraph(assets: Assets) {
const state = this.getState(); const state = this.getState();
if ( if (
state.type !== "INITIALIZED" || state.type !== "INITIALIZED" ||
@ -176,7 +183,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
this.setState(loadingState); this.setState(loadingState);
let newState: ?AppState; let newState: ?AppState;
try { try {
const graphWithAdapters = await this.loadGraphWithAdapters(repo); const graphWithAdapters = await this.loadGraphWithAdapters(assets, repo);
newState = { newState = {
...state, ...state,
substate: { substate: {
@ -250,8 +257,9 @@ export type GraphWithAdapters = {|
+adapters: DynamicAdapterSet, +adapters: DynamicAdapterSet,
|}; |};
export async function loadGraphWithAdapters( export async function loadGraphWithAdapters(
assets: Assets,
repo: Repo repo: Repo
): Promise<GraphWithAdapters> { ): Promise<GraphWithAdapters> {
const adapters = await defaultStaticAdapters().load(repo); const adapters = await defaultStaticAdapters().load(assets, repo);
return {graph: adapters.graph(), adapters}; return {graph: adapters.graph(), adapters};
} }

View File

@ -8,6 +8,7 @@ import {
} from "./state"; } from "./state";
import {Graph} from "../../core/graph"; import {Graph} from "../../core/graph";
import {Assets} from "../assets";
import {makeRepo, type Repo} from "../../core/repo"; import {makeRepo, type Repo} from "../../core/repo";
import {type EdgeEvaluator} from "../../core/attribution/pagerank"; import {type EdgeEvaluator} from "../../core/attribution/pagerank";
import {StaticAdapterSet, DynamicAdapterSet} from "../adapters/adapterSet"; import {StaticAdapterSet, DynamicAdapterSet} from "../adapters/adapterSet";
@ -23,7 +24,10 @@ describe("app/credExplorer/state", () => {
const setState = (appState) => { const setState = (appState) => {
stateContainer.appState = appState; stateContainer.appState = appState;
}; };
const loadGraphMock: (repo: Repo) => Promise<GraphWithAdapters> = jest.fn(); const loadGraphMock: (
assets: Assets,
repo: Repo
) => Promise<GraphWithAdapters> = jest.fn();
const pagerankMock: ( const pagerankMock: (
Graph, Graph,
EdgeEvaluator, EdgeEvaluator,
@ -196,13 +200,25 @@ describe("app/credExplorer/state", () => {
]; ];
for (const b of badStates) { for (const b of badStates) {
const {stm} = example(b); const {stm} = example(b);
await expect(stm.loadGraph()).rejects.toThrow("incorrect state"); await expect(stm.loadGraph(new Assets("/my/gateway/"))).rejects.toThrow(
"incorrect state"
);
} }
}); });
it("passes along the adapters and repo", () => {
const {stm, loadGraphMock} = example(readyToLoadGraph());
expect(loadGraphMock).toHaveBeenCalledTimes(0);
const assets = new Assets("/my/gateway/");
stm.loadGraph(assets);
expect(loadGraphMock).toHaveBeenCalledTimes(1);
expect(loadGraphMock.mock.calls[0]).toHaveLength(2);
expect(loadGraphMock.mock.calls[0][0]).toBe(assets);
expect(loadGraphMock.mock.calls[0][1]).toEqual(makeRepo("foo", "bar"));
});
it("immediately sets loading status", () => { it("immediately sets loading status", () => {
const {getState, stm} = example(readyToLoadGraph()); const {getState, stm} = example(readyToLoadGraph());
expect(loading(getState())).toBe("NOT_LOADING"); expect(loading(getState())).toBe("NOT_LOADING");
stm.loadGraph(); stm.loadGraph(new Assets("/my/gateway/"));
expect(loading(getState())).toBe("LOADING"); expect(loading(getState())).toBe("LOADING");
expect(getSubstate(getState()).type).toBe("READY_TO_LOAD_GRAPH"); expect(getSubstate(getState()).type).toBe("READY_TO_LOAD_GRAPH");
}); });
@ -210,7 +226,7 @@ describe("app/credExplorer/state", () => {
const {getState, stm, loadGraphMock} = example(readyToLoadGraph()); const {getState, stm, loadGraphMock} = example(readyToLoadGraph());
const gwa = graphWithAdapters(); const gwa = graphWithAdapters();
loadGraphMock.mockResolvedValue(gwa); loadGraphMock.mockResolvedValue(gwa);
await stm.loadGraph(); await stm.loadGraph(new Assets("/my/gateway/"));
const state = getState(); const state = getState();
const substate = getSubstate(state); const substate = getSubstate(state);
expect(loading(state)).toBe("NOT_LOADING"); expect(loading(state)).toBe("NOT_LOADING");
@ -230,7 +246,7 @@ describe("app/credExplorer/state", () => {
resolve(graphWithAdapters()); resolve(graphWithAdapters());
}) })
); );
await stm.loadGraph(); await stm.loadGraph(new Assets("/my/gateway/"));
const state = getState(); const state = getState();
const substate = getSubstate(state); const substate = getSubstate(state);
expect(loading(state)).toBe("NOT_LOADING"); expect(loading(state)).toBe("NOT_LOADING");
@ -243,7 +259,7 @@ describe("app/credExplorer/state", () => {
// $ExpectFlowError // $ExpectFlowError
console.error = jest.fn(); console.error = jest.fn();
loadGraphMock.mockRejectedValue(error); loadGraphMock.mockRejectedValue(error);
await stm.loadGraph(); await stm.loadGraph(new Assets("/my/gateway/"));
const state = getState(); const state = getState();
const substate = getSubstate(state); const substate = getSubstate(state);
expect(loading(state)).toBe("FAILED"); expect(loading(state)).toBe("FAILED");

View File

@ -7,6 +7,7 @@ import {Graph} from "../../core/graph";
import * as N from "./nodes"; import * as N from "./nodes";
import * as E from "./edges"; import * as E from "./edges";
import {description} from "./render"; import {description} from "./render";
import type {Assets} from "../../app/assets";
import type {Repo} from "../../core/repo"; import type {Repo} from "../../core/repo";
export class StaticPluginAdapter implements IStaticPluginAdapter { export class StaticPluginAdapter implements IStaticPluginAdapter {
@ -76,8 +77,10 @@ export class StaticPluginAdapter implements IStaticPluginAdapter {
}, },
]; ];
} }
async load(repo: Repo): Promise<IDynamicPluginAdapter> { async load(assets: Assets, repo: Repo): Promise<IDynamicPluginAdapter> {
const url = `/api/v1/data/data/${repo.owner}/${repo.name}/git/graph.json`; const url = assets.resolve(
`/api/v1/data/data/${repo.owner}/${repo.name}/git/graph.json`
);
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
return Promise.reject(response); return Promise.reject(response);

View File

@ -9,6 +9,7 @@ import * as N from "./nodes";
import * as E from "./edges"; import * as E from "./edges";
import {RelationalView} from "./relationalView"; import {RelationalView} from "./relationalView";
import {description} from "./render"; import {description} from "./render";
import type {Assets} from "../../app/assets";
import type {Repo} from "../../core/repo"; import type {Repo} from "../../core/repo";
export class StaticPluginAdapter implements IStaticPluginAdapter { export class StaticPluginAdapter implements IStaticPluginAdapter {
@ -85,8 +86,10 @@ export class StaticPluginAdapter implements IStaticPluginAdapter {
}, },
]; ];
} }
async load(repo: Repo): Promise<IDynamicPluginAdapater> { async load(assets: Assets, repo: Repo): Promise<IDynamicPluginAdapater> {
const url = `/api/v1/data/data/${repo.owner}/${repo.name}/github/view.json`; const url = assets.resolve(
`/api/v1/data/data/${repo.owner}/${repo.name}/github/view.json`
);
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
return Promise.reject(response); return Promise.reject(response);