Merge branch 'next' into next-static-uri

This commit is contained in:
Wyatt Johnson
2018-10-15 22:47:04 +00:00
committed by GitHub
27 changed files with 646 additions and 186 deletions
+4 -1
View File
@@ -9,7 +9,10 @@ module.exports = {
env: {
test: {
presets: [
["@babel/env", { targets: "last 2 versions, ie 11", modules: false }],
[
"@babel/env",
{ targets: "last 2 versions, ie 11", modules: "commonjs" },
],
"@babel/react",
],
},
+1 -1
View File
@@ -24,7 +24,7 @@ module.exports = {
"<rootDir>/config/jest/fileTransform.js",
},
transformIgnorePatterns: [
"[/\\\\]node_modules[/\\\\](?!(fluent)[/\\\\]).+\\.(js|jsx|mjs|ts|tsx)$",
"[/\\\\]node_modules[/\\\\](?!(fluent|react-relay-network-modern)[/\\\\]).+\\.(js|jsx|mjs|ts|tsx)$",
],
moduleNameMapper: {
"^talk-admin/(.*)$": "<rootDir>/src/core/client/admin/$1",
+5
View File
@@ -21995,6 +21995,11 @@
"relay-runtime": "1.7.0-rc.1"
}
},
"react-relay-network-modern": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/react-relay-network-modern/-/react-relay-network-modern-2.4.0.tgz",
"integrity": "sha512-LR/RhHcJclDCVEiwRhlRtf1iltSnbGSxS2rag+bAljMFJ0kOVSYUK3+NDPRbcHLRqbha1FuQXBVfHjjPE6jhMA=="
},
"react-responsive": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-5.0.0.tgz",
+1
View File
@@ -94,6 +94,7 @@
"passport-strategy": "^1.0.0",
"performance-now": "^2.1.0",
"permit": "^0.2.4",
"react-relay-network-modern": "^2.4.0",
"striptags": "^3.1.1",
"subscriptions-transport-ws": "^0.9.12",
"tlds": "^1.203.1",
@@ -4,7 +4,7 @@ import { noop } from "lodash";
import { Child as PymChild } from "pym.js";
import React, { Component, ComponentType } from "react";
import { Formatter } from "react-timeago";
import { Environment, Network, RecordSource, Store } from "relay-runtime";
import { Environment, RecordSource, Store } from "relay-runtime";
import uuid from "uuid/v4";
import { getBrowserInfo } from "talk-framework/lib/browserInfo";
@@ -21,7 +21,7 @@ import { RestClient } from "talk-framework/lib/rest";
import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside";
import { generateBundles, LocalesData, negotiateLanguages } from "../i18n";
import { createFetch, TokenGetter } from "../network";
import { createNetwork, TokenGetter } from "../network";
import { PostMessageService } from "../postMessage";
import { TalkContext, TalkContextProvider } from "./TalkContext";
@@ -97,7 +97,7 @@ function createRelayEnvironment() {
return "";
};
const environment = new Environment({
network: Network.create(createFetch(tokenGetter)),
network: createNetwork(tokenGetter),
store: new Store(source),
});
return { environment, tokenGetter };
@@ -1,25 +0,0 @@
export interface GraphQLErrorItem {
message: string;
locations: Array<{
line: number;
column: number;
}>;
}
/**
* Graphql wraps graphql errors at the network layer.
*/
export default class GraphQLError extends Error {
// Original error.
public readonly origin: GraphQLErrorItem[];
constructor(origin: GraphQLErrorItem[]) {
super(origin.map(o => o.message).join(" "));
// Maintains proper stack trace for where our error was thrown.
if (Error.captureStackTrace) {
Error.captureStackTrace(this, GraphQLError);
}
this.origin = origin;
}
}
@@ -1,6 +1,2 @@
export { default as NetworkError } from "./networkError";
export { default as UnknownServerError } from "./unknownServerError";
export { default as BadUserInputError } from "./badUserInputError";
export { default as GraphQLError } from "./graphqlError";
export * from "./graphqlError";
@@ -1,18 +0,0 @@
/**
* NetworkError wraps errors at the network layer.
*/
export default class NetworkError extends Error {
// Original error.
public readonly origin: Error;
constructor(origin: Error) {
// Pass remaining arguments (including vendor specific ones) to parent constructor.
super(origin.message);
// Maintains proper stack trace for where our error was thrown.
if (Error.captureStackTrace) {
Error.captureStackTrace(this, NetworkError);
}
this.origin = origin;
}
}
@@ -0,0 +1,40 @@
import {
authMiddleware,
batchMiddleware,
cacheMiddleware,
RelayNetworkLayer,
retryMiddleware,
urlMiddleware,
} from "react-relay-network-modern/es";
import customErrorMiddleware from "./customErrorMiddleware";
export type TokenGetter = () => string;
const graphqlURL = "/api/tenant/graphql";
export default function createNetwork(tokenGetter: TokenGetter) {
return new RelayNetworkLayer([
customErrorMiddleware,
cacheMiddleware({
size: 100, // max 100 requests
ttl: 900000, // 15 minutes
}),
urlMiddleware({
url: req => Promise.resolve(graphqlURL),
}),
batchMiddleware({
batchUrl: (requestMap: any) => Promise.resolve(graphqlURL),
batchTimeout: 10,
}),
retryMiddleware({
fetchTimeout: 15000,
retryDelays: (attempt: number) => Math.pow(2, attempt + 4) * 100,
// or simple array [3200, 6400, 12800, 25600, 51200, 102400, 204800, 409600],
statusCodes: [500, 503, 504],
}),
authMiddleware({
token: tokenGetter,
}),
]);
}
@@ -0,0 +1,31 @@
import { Middleware } from "react-relay-network-modern/es";
import { BadUserInputError, UnknownServerError } from "../errors";
function getError(errors: Error[]): Error | null {
if (errors.length > 1 || !(errors[0] as any).extensions) {
// Multiple errors are GraphQL errors.
// TODO: (cvle) Is this assumption correct?
// No extensions == GraphQL error.
// TODO: (cvle) harmonize with server.
return null;
}
const err = errors[0];
if ((err as any).code === "BAD_USER_INPUT") {
return new BadUserInputError((err as any).extensions);
}
return new UnknownServerError(err.message, (err as any).extensions);
}
const customErrorMiddleware: Middleware = next => async req => {
const res = await next(req);
if (req.isMutation() && res.errors) {
// Extract custom error.
const error = getError(res.errors);
if (error) {
throw error;
}
}
return res;
};
export default customErrorMiddleware;
@@ -1,72 +0,0 @@
import { FetchFunction } from "relay-runtime";
import {
BadUserInputError,
GraphQLError,
NetworkError,
UnknownServerError,
} from "../errors";
// Normalize errors.
function getError(errors: Error[]): Error {
if (errors.length > 1) {
// Multiple errors are GraphQL errors.
// TODO: (cvle) Is this assumption correct?
return new GraphQLError(errors as any);
}
const err = errors[0] as Error;
if ((err as any).extensions) {
if ((err as any).code === "BAD_USER_INPUT") {
return new BadUserInputError((err as any).extensions);
}
return new UnknownServerError(err.message, (err as any).extensions);
}
// No extensions == GraphQL error.
// TODO: (cvle) harmonize with server.
return new GraphQLError(errors as any);
}
export type TokenGetter = () => string;
type CreateFetch = (token?: TokenGetter) => FetchFunction;
/**
* createFetch returns a simple implementation of the `FetchFunction`
* required by Relay. It'll return a `NetworkError` on failure.
*/
const createFetch: CreateFetch = tokenGetter => async (
operation,
variables
) => {
const token = tokenGetter && tokenGetter();
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
try {
const response = await fetch("/api/tenant/graphql", {
method: "POST",
headers,
body: JSON.stringify({
query: operation.text,
variables,
}),
});
if (response.status >= 500) {
throw new Error(`${response.status} ${response.statusText}`);
}
const data = await response.json();
if (data.errors) {
throw getError(data.errors);
}
return data;
} catch (err) {
if (err instanceof TypeError) {
throw new NetworkError(err);
}
throw err;
}
};
export default createFetch;
@@ -1 +1 @@
export { default as createFetch, TokenGetter } from "./fetchQuery";
export { default as createNetwork, TokenGetter } from "./createNetwork";
@@ -24,17 +24,19 @@ function sharedUpdater(
store: RecordSourceSelectorProxy,
input: CreateCommentInput
) {
updateAsset(store, input);
if (input.local) {
localUpdate(store, input);
} else {
update(store, input);
}
updateProfile(store, input);
}
/**
* update integrates new comment into the CommentConnection.
*/
function update(store: RecordSourceSelectorProxy, input: CreateCommentInput) {
function updateAsset(
store: RecordSourceSelectorProxy,
input: CreateCommentInput
) {
// Updating Comment Count
const asset = store.get(input.assetID);
if (asset) {
@@ -45,7 +47,12 @@ function update(store: RecordSourceSelectorProxy, input: CreateCommentInput) {
record.setValue(currentCount + 1, "totalVisible");
}
}
}
/**
* update integrates new comment into the CommentConnection.
*/
function update(store: RecordSourceSelectorProxy, input: CreateCommentInput) {
// Get the payload returned from the server.
const payload = store.getRootField("createComment")!;
@@ -109,6 +116,26 @@ function localUpdate(
}
}
/**
* updateProfile integrates new comment into the profile.
*/
function updateProfile(
store: RecordSourceSelectorProxy,
input: CreateCommentInput
) {
// Get the payload returned from the server.
const payload = store.getRootField("createComment")!;
// Get the edge of the newly created comment.
const newEdge = payload.getLinkedRecord("edge")!;
const newComment = newEdge.getLinkedRecord("node");
// TODO: update profile comments connection after we
// integrated pagination.
// tslint:disable-next-line:no-unused-expression
newComment;
}
const mutation = graphql`
mutation CreateCommentMutation($input: CreateCommentInput!) {
createComment(input: $input) {
@@ -64,6 +64,10 @@ const ProfileQuery: StatelessComponent<InnerProps> = ({
assetID,
assetURL,
}}
cacheConfig={{
// TODO: enable caching after mutations are adapted.
force: true,
}}
render={render}
/>
);
@@ -1003,7 +1003,7 @@ exports[`post a reply: optimistic response 1`] = `
role="tab"
type="button"
>
1 Comment
2 Comments
</button>
</li>
<li
@@ -2072,7 +2072,7 @@ exports[`post a reply: server response 1`] = `
role="tab"
type="button"
>
1 Comment
2 Comments
</button>
</li>
<li
@@ -43,7 +43,7 @@ export const signupHandler = (options: SignupOptions): RequestHandler => async (
// TODO: rate limit based on the IP address and user agent.
// Tenant is guaranteed at this point.
const tenant = req.tenant!;
const tenant = req.talk!.tenant!;
// Check to ensure that the local integration has been enabled.
if (!tenant.auth.integrations.local.enabled) {
@@ -95,7 +95,7 @@ export const logoutHandler = (options: LogoutOptions): RequestHandler => async (
// TODO: rate limit based on the IP address and user agent.
// Tenant is guaranteed at this point.
const tenant = req.tenant!;
const tenant = req.talk!.tenant!;
// Check to ensure that the local integration has been enabled.
if (!tenant.auth.integrations.local.enabled) {
@@ -0,0 +1,51 @@
import { RequestHandler } from "express-jwt";
import { Redis } from "ioredis";
import { Db } from "mongodb";
import TenantContext from "talk-server/graph/tenant/context";
import { TaskQueue } from "talk-server/services/queue";
import { Request } from "talk-server/types/express";
export interface TenantContextMiddlewareOptions {
mongo: Db;
redis: Redis;
queue: TaskQueue;
}
export const tenantContext = ({
mongo,
redis,
queue,
}: TenantContextMiddlewareOptions): RequestHandler => (
req: Request,
res,
next
) => {
if (!req.talk) {
return next(new Error("talk was not set"));
}
const { tenant, cache } = req.talk;
if (!cache) {
return next(new Error("cache was not set"));
}
if (!tenant) {
return next(new Error("tenant was not set"));
}
req.talk.context = {
tenant: new TenantContext({
req,
mongo,
redis,
tenant,
user: req.user,
tenantCache: cache.tenant,
queue,
}),
};
next();
};
@@ -0,0 +1,97 @@
import { RequestHandler, Response } from "express";
import logger from "talk-server/logger";
import { Request } from "talk-server/types/express";
function wrapResponse(req: Request, res: Response) {
// If the request is not an array, or has no elements, we should skip it.
if (!Array.isArray(req.body) || req.body.length === 0) {
return res;
}
// If the request is an array, but it does not have an ID field, then we
// should skip it.
const needsUpgrade = Boolean(typeof req.body[0].id !== "undefined");
if (!needsUpgrade) {
return res;
}
// Grab all the existing ID's.
const ids: string[] = req.body.map(({ id }) => id);
// Save a reference to the old setHeader function.
const setHeader = res.setHeader.bind(res);
// Capture all the headers that are sent to this, in case we need to use it.
const setHeaders: Record<string, any> = {};
res.setHeader = (name: string, value: any) => {
setHeaders[name] = value;
return res;
};
// Save a reference to the old write function.
const write = res.write.bind(res);
// Create a flush function that will be used to flush the response to the
// underlying response.
const flush = (chunk: any, headers: Record<string, any> = setHeaders) => {
for (const name in headers) {
if (!headers.hasOwnProperty(name)) {
continue;
}
setHeader(name, headers[name]);
}
return write(chunk);
};
// Override the response writer to parse the response to determine if it needs
// to be rewritten.
res.write = (chunk: string) => {
try {
// If there is no response, forward it, or if we peek at the first
// character and it's not an array opening, then skip it.
if (chunk.length <= 0 || chunk[0] !== "[") {
return flush(chunk);
}
// Parse the responses, if it's not an array, then skip it.
const responses: object[] | any = JSON.parse(chunk);
if (!Array.isArray(responses) || responses.length === 0) {
return flush(chunk);
}
// If the length of responses do not equal the length of id's collected,
// then skip it.
if (responses.length !== ids.length) {
return flush(chunk);
}
// For each of the responses, zip up their id's into the objects, and
// string concat them together to ensure we get the right request.
const gqlResponse = responses.reduce((body: object[], payload, idx) => {
const id = ids[idx];
body.push({ id, payload });
return body;
}, []);
const response = JSON.stringify(gqlResponse);
return flush(response, {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(response, "utf8").toString(),
});
} catch (err) {
logger.error({ err }, "could not parse chunk as JSON");
return flush(chunk);
}
};
return res;
}
export const graphqlBatchMiddleware = (
graphqlRequestHandler: RequestHandler
): RequestHandler => (req: Request, res, next) =>
graphqlRequestHandler(req, wrapResponse(req, res), next);
@@ -102,8 +102,8 @@ export async function handleSuccessfulLogin(
next: NextFunction
) {
try {
// Grab the tenant from the request.
const { tenant } = req;
// Talk is guaranteed at this point.
const { tenant } = req.talk!;
const options: SigningTokenOptions = {};
@@ -100,7 +100,7 @@ export class JWTStrategy extends Strategy {
return this.pass();
}
const { tenant } = req;
const { tenant } = req.talk!;
if (!tenant) {
// TODO: (wyattjoh) log this error, and return a better one?
return this.error(new Error("tenant not found"));
@@ -18,7 +18,7 @@ const verifyFactory = (mongo: Db) => async (
// TODO: rate limit based on the IP address and user agent.
// The tenant is guaranteed at this point.
const tenant = req.tenant!;
const tenant = req.talk!.tenant!;
// Get the user from the database.
const user = await retrieveUserWithProfile(mongo, tenant.id, {
@@ -265,7 +265,7 @@ export default class OIDCStrategy extends Strategy {
}
// Grab the tenant out of the request, as we need some more details.
const { tenant } = req;
const { tenant } = req.talk!;
if (!tenant) {
// TODO: return a better error.
return done(new Error("tenant not found"));
@@ -336,7 +336,7 @@ export default class OIDCStrategy extends Strategy {
}
private async lookupStrategy(req: Request) {
const { tenant } = req;
const { tenant } = req.talk!;
if (!tenant) {
// TODO: return a better error.
throw new Error("tenant not found");
+10 -8
View File
@@ -7,14 +7,12 @@ export interface MiddlewareOptions {
cache: TenantCache;
}
export default (options: MiddlewareOptions): RequestHandler => async (
export default ({ cache }: MiddlewareOptions): RequestHandler => async (
req: Request,
res,
next
) => {
try {
const { cache } = options;
// Attach the tenant to the request.
const tenant = await cache.retrieveByDomain(req.hostname);
if (!tenant) {
@@ -22,11 +20,15 @@ export default (options: MiddlewareOptions): RequestHandler => async (
return next(new Error("tenant not found"));
}
// Attach the tenant cache to the request.
req.tenantCache = cache;
// Attach the tenant to the request.
req.tenant = tenant;
// Set Talk on the request.
req.talk = {
cache: {
// Attach the tenant cache to the request.
tenant: cache,
},
// Attach the tenant to the request.
tenant,
};
// Attach the tenant to the view locals.
res.locals.tenant = tenant;
+6 -3
View File
@@ -6,6 +6,7 @@ import tenantMiddleware from "talk-server/app/middleware/tenant";
import { RouterOptions } from "talk-server/app/router/types";
import tenantGraphMiddleware from "talk-server/graph/tenant/middleware";
import { tenantContext } from "talk-server/app/middleware/context/tenant";
import { createNewAuthRouter } from "./auth";
export async function createTenantRouter(
@@ -41,12 +42,14 @@ export async function createTenantRouter(
// Any users may submit their GraphQL requests with authentication, this
// middleware will unpack their user into the request.
options.passport.authenticate("jwt", { session: false }),
await tenantGraphMiddleware({
schema: app.schemas.tenant,
config: app.config,
tenantContext({
mongo: app.mongo,
redis: app.redis,
queue: app.queue,
}),
await tenantGraphMiddleware({
schema: app.schemas.tenant,
config: app.config,
})
);
+25 -33
View File
@@ -1,45 +1,37 @@
import { GraphQLSchema } from "graphql";
import { Redis } from "ioredis";
import { Db } from "mongodb";
// import { graphqlBatchHTTPWrapper } from "react-relay-network-layer";
import { Config } from "talk-common/config";
import { graphqlBatchMiddleware } from "talk-server/app/middleware/graphqlBatch";
import { graphqlMiddleware } from "talk-server/graph/common/middleware";
import { TaskQueue } from "talk-server/services/queue";
import { Request } from "talk-server/types/express";
import TenantContext from "./context";
export interface TenantGraphQLMiddlewareOptions {
schema: GraphQLSchema;
config: Config;
mongo: Db;
redis: Redis;
queue: TaskQueue;
}
export default async ({
schema,
config,
mongo,
redis,
queue,
}: TenantGraphQLMiddlewareOptions) => {
return graphqlMiddleware(config, async (req: Request) => {
// Load the tenant and user from the request.
const { tenant, user, tenantCache } = req;
export default async ({ schema, config }: TenantGraphQLMiddlewareOptions) =>
graphqlBatchMiddleware(
graphqlMiddleware(config, async (req: Request) => {
if (!req.talk) {
throw new Error("talk was not set");
}
// Return the graph options.
return {
schema,
context: new TenantContext({
req,
mongo,
redis,
tenant: tenant!,
user,
tenantCache,
queue,
}),
};
});
};
const { context } = req.talk;
if (!context) {
throw new Error("context was not set");
}
const { tenant } = context;
if (!tenant) {
throw new Error("tenant was not set");
}
// Return the graph options.
return {
schema,
context: tenant,
};
})
);
+13 -3
View File
@@ -1,11 +1,21 @@
import { Request } from "express";
import TenantContext from "talk-server/graph/tenant/context";
import { Tenant } from "talk-server/models/tenant";
import { User } from "talk-server/models/user";
import TenantCache from "talk-server/services/tenant/cache";
export interface Request extends Request {
user?: User;
export interface TalkRequest {
cache?: {
tenant: TenantCache;
};
tenant?: Tenant;
tenantCache: TenantCache;
context?: {
tenant?: TenantContext;
};
}
export interface Request extends Request {
talk?: TalkRequest;
user?: User;
}
+313
View File
@@ -0,0 +1,313 @@
/* tslint:disable */
// TODO: send a PR to DefinitelyTyped.
declare module "react-relay-network-modern/es" {
// TODO: missing typescript types.
// import { QueryResponseCache } from 'relay-runtime';
export interface Variables {
[name: string]: any;
}
export interface FetchOpts {
url?: string;
method: "POST" | "GET";
headers: { [name: string]: string };
body: string | FormData;
// Avaliable request modes in fetch options. For details see https://fetch.spec.whatwg.org/#requests
credentials?: "same-origin" | "include" | "omit";
mode?: "cors" | "websocket" | "navigate" | "no-cors" | "same-origin";
cache?:
| "default"
| "no-store"
| "reload"
| "no-cache"
| "force-cache"
| "only-if-cached";
redirect?: "follow" | "error" | "manual";
[name: string]: any;
}
export interface ConcreteBatch {
kind: "Batch";
fragment: any;
id?: string | null;
metadata: { [key: string]: any };
name: string;
query: any;
text?: string | null;
}
export interface CacheConfig {
force?: boolean | null;
poll?: number | null;
rerunParamExperimental?: any;
}
export type FetchResponse = Response;
export type Uploadable = File | Blob;
export interface UploadableMap {
[key: string]: Uploadable;
}
export class RelayRequest {
public static lastGenId: number;
public id: string;
public fetchOpts: FetchOpts;
public operation: ConcreteBatch;
public variables: Variables;
public cacheConfig: CacheConfig;
public uploadables?: UploadableMap | null;
public getBody(): string | FormData;
public prepareBody(): string | FormData;
public getID(): string;
public getQueryString(): string;
public getVariables(): Variables;
public isMutation(): boolean;
public isFormData(): boolean;
public clone(): RelayRequest;
}
export class RelayResponse {
public data?: PayloadData | null;
public errors?: any[] | null;
public ok: any;
public status: number;
public statusText?: string | null;
public headers?: { [name: string]: string } | null;
public url?: string | null;
public text?: string | null;
public json: any;
public static createFromFetch(res: FetchResponse): Promise<RelayResponse>;
public static createFromGraphQL(res: {
errors?: any;
data?: any;
}): Promise<RelayResponse>;
public processJsonData(json: any): void;
public clone(): RelayResponse;
public toString(): string;
}
export interface PayloadData {
[key: string]: any;
}
export type QueryPayload =
| {
data?: PayloadData | null;
errors?: any[];
rerunVariables?: Variables;
}
| RelayResponse;
// this is workaround should be class from relay-runtime/network/RelayObservable.js
export type RelayObservable<T> = Promise<T>;
// Note: This should accept Subscribable<T> instead of RelayObservable<T>,
// however Flow cannot yet distinguish it from T.
export type ObservableFromValue<T> = RelayObservable<T> | Promise<T> | T;
export type FetchHookFunction = (
operation: ConcreteBatch,
variables: Variables,
cacheConfig: CacheConfig,
uploadables?: UploadableMap | null
) => void | ObservableFromValue<QueryPayload>;
export interface RelayNetworkLayerOpts {
subscribeFn?: SubscribeFunction;
beforeFetch?: FetchHookFunction;
noThrow?: boolean;
}
export interface Disposable {
dispose(): void;
}
export type SubscribeFunction = (
operation: ConcreteBatch,
variables: Variables,
cacheConfig: CacheConfig,
observer: any
) => RelayObservable<QueryPayload> | Disposable;
export type Requests = RelayRequest[];
export default class RelayRequestBatch {
public fetchOpts: Partial<FetchOpts>;
public requests: Requests;
constructor(requests: Requests);
public setFetchOption(name: string, value: any): void;
public setFetchOptions(opts: Object): void;
public getBody(): string;
public prepareBody(): string;
public getIds(): string[];
public getID(): string;
public isMutation(): boolean;
public isFormData(): boolean;
public clone(): RelayRequestBatch;
public getVariables(): Variables;
public getQueryString(): string;
}
export type RelayRequestAny = RelayRequest | RelayRequestBatch;
export type MiddlewareNextFn = (
req: RelayRequestAny
) => Promise<RelayResponse>;
export type Middleware = (next: MiddlewareNextFn) => MiddlewareNextFn;
export type MiddlewareRawNextFn = (
req: RelayRequestAny
) => Promise<FetchResponse>;
export interface MiddlewareRaw {
isRawMiddleware: true;
(next: MiddlewareRawNextFn): MiddlewareRawNextFn;
}
export interface MiddlewareSync {
execute: (
operation: ConcreteBatch,
variables: Variables,
cacheConfig: CacheConfig,
uploadables?: UploadableMap | null
) => ObservableFromValue<QueryPayload>;
}
export class RelayNetworkLayer {
constructor(
middlewares: Array<Middleware | MiddlewareSync | MiddlewareRaw>,
opts?: RelayNetworkLayerOpts
);
}
export interface AuthMiddlewareOpts {
token?:
| string
| Promise<string>
| ((req: RelayRequestAny) => string | Promise<string>);
tokenRefreshPromise?: (
req: RelayRequestAny,
res: RelayResponse
) => string | Promise<string>;
allowEmptyToken?: boolean;
prefix?: string;
header?: string;
}
export const authMiddleware: (opts?: AuthMiddlewareOpts) => Middleware;
export interface BatchRequestMap {
[reqId: string]: RequestWrapper;
}
export interface RequestWrapper {
req: RelayRequest;
completeOk: (res: Object) => void;
completeErr: (e: Error) => void;
done: boolean;
duplicates: RequestWrapper[];
}
export interface BatchMiddlewareOpts {
batchUrl?:
| string
| Promise<string>
| ((requestMap: BatchRequestMap) => string | Promise<string>);
batchTimeout?: number;
maxBatchSize?: number;
allowMutations?: boolean;
method?: "POST" | "GET";
headers?:
| Headers
| Promise<Headers>
| ((req: RelayRequestBatch) => Headers | Promise<Headers>);
// Avaliable request modes in fetch options. For details see https://fetch.spec.whatwg.org/#requests
credentials?: FetchOpts["credentials"];
mode?: FetchOpts["mode"];
cache?: FetchOpts["cache"];
redirect?: FetchOpts["redirect"];
}
export const batchMiddleware: (opts?: BatchMiddlewareOpts) => Middleware;
interface CacheMiddlewareOpts {
size?: number;
ttl?: number;
onInit?: (cache: any /* TODO: missing type QueryResponseCache */) => any;
allowMutations?: boolean;
allowFormData?: boolean;
clearOnMutation?: boolean;
}
export const cacheMiddleware: (opts?: CacheMiddlewareOpts) => Middleware;
export interface GqlErrorMiddlewareOpts {
logger?: Function;
prefix?: string;
disableServerMiddlewareTip?: boolean;
}
export const errorMiddleware: (opts?: GqlErrorMiddlewareOpts) => Middleware;
export interface LoggerMiddlewareOpts {
logger?: Function;
}
export const loggerMiddleware: (opts?: LoggerMiddlewareOpts) => Middleware;
export interface PerfMiddlewareOpts {
logger?: Function;
}
export const performanceMiddleware: (opts?: PerfMiddlewareOpts) => Middleware;
export interface ProgressOpts {
sizeHeader?: string;
onProgress: (runningTotal: number, totalSize?: number | null) => any;
}
export const progressMiddleware: (opts?: ProgressOpts) => Middleware;
export type RetryAfterFn = (attempt: number) => number | false;
export type ForceRetryFn = (runNow: Function, delay: number) => any;
export type StatusCheckFn = (
statusCode: number,
req: RelayRequestAny,
res: RelayResponse
) => boolean;
export interface RetryMiddlewareOpts {
fetchTimeout?: number;
retryDelays?: number[] | RetryAfterFn;
statusCodes?: number[] | false | StatusCheckFn;
logger?: Function | false;
allowMutations?: boolean;
allowFormData?: boolean;
forceRetry?: ForceRetryFn | false;
}
export const retryMiddleware: (opts?: RetryMiddlewareOpts) => Middleware;
export interface UrlMiddlewareOpts {
url:
| string
| Promise<string>
| ((req: RelayRequest) => string | Promise<string>);
method?: "POST" | "GET";
headers?:
| Headers
| Promise<Headers>
| ((req: RelayRequest) => Headers | Promise<Headers>);
// Avaliable request modes in fetch options. For details see https://fetch.spec.whatwg.org/#requests
credentials?: FetchOpts["credentials"];
mode?: FetchOpts["mode"];
cache?: FetchOpts["cache"];
redirect?: FetchOpts["redirect"];
}
export const urlMiddleware: (opts?: UrlMiddlewareOpts) => Middleware;
}