apollo-angular
Version:
Use your GraphQL data in your Angular app, with the Apollo Client
349 lines (342 loc) • 13.6 kB
JavaScript
import { print } from 'graphql';
import * as i0 from '@angular/core';
import { Injectable } from '@angular/core';
import { ApolloLink, Observable as Observable$1 } from '@apollo/client/core';
import { BatchLink } from '@apollo/client/link/batch';
import { Observable } from 'rxjs';
import * as i1 from '@angular/common/http';
import { HttpHeaders } from '@angular/common/http';
const fetch = (req, httpClient, extractFiles) => {
const shouldUseBody = ['POST', 'PUT', 'PATCH'].indexOf(req.method.toUpperCase()) !== -1;
const shouldStringify = (param) => ['variables', 'extensions'].indexOf(param.toLowerCase()) !== -1;
const isBatching = req.body.length;
let shouldUseMultipart = req.options && req.options.useMultipart;
let multipartInfo;
if (shouldUseMultipart) {
if (isBatching) {
return new Observable(observer => observer.error(new Error('File upload is not available when combined with Batching')));
}
if (!shouldUseBody) {
return new Observable(observer => observer.error(new Error('File upload is not available when GET is used')));
}
if (!extractFiles) {
return new Observable(observer => observer.error(new Error(`To use File upload you need to pass "extractFiles" function from "extract-files" library to HttpLink's options`)));
}
multipartInfo = extractFiles(req.body);
shouldUseMultipart = !!multipartInfo.files.size;
}
// `body` for some, `params` for others
let bodyOrParams = {};
if (isBatching) {
if (!shouldUseBody) {
return new Observable(observer => observer.error(new Error('Batching is not available for GET requests')));
}
bodyOrParams = {
body: req.body,
};
}
else {
const body = shouldUseMultipart ? multipartInfo.clone : req.body;
if (shouldUseBody) {
bodyOrParams = {
body,
};
}
else {
const params = Object.keys(req.body).reduce((obj, param) => {
const value = req.body[param];
obj[param] = shouldStringify(param) ? JSON.stringify(value) : value;
return obj;
}, {});
bodyOrParams = { params: params };
}
}
if (shouldUseMultipart && shouldUseBody) {
const form = new FormData();
form.append('operations', JSON.stringify(bodyOrParams.body));
const map = {};
const files = multipartInfo.files;
let i = 0;
files.forEach(paths => {
map[++i] = paths;
});
form.append('map', JSON.stringify(map));
i = 0;
files.forEach((_, file) => {
form.append(++i + '', file, file.name);
});
bodyOrParams.body = form;
}
// create a request
return httpClient.request(req.method, req.url, {
observe: 'response',
responseType: 'json',
reportProgress: false,
...bodyOrParams,
...req.options,
});
};
const mergeHeaders = (source, destination) => {
if (source && destination) {
const merged = destination
.keys()
.reduce((headers, name) => headers.set(name, destination.getAll(name)), source);
return merged;
}
return destination || source;
};
function prioritize(...values) {
return values.find(val => typeof val !== 'undefined');
}
function createHeadersWithClientAwareness(context) {
// `apollographql-client-*` headers are automatically set if a
// `clientAwareness` object is found in the context. These headers are
// set first, followed by the rest of the headers pulled from
// `context.headers`.
let headers = context.headers && context.headers instanceof HttpHeaders
? context.headers
: new HttpHeaders(context.headers);
if (context.clientAwareness) {
const { name, version } = context.clientAwareness;
// If desired, `apollographql-client-*` headers set by
// the `clientAwareness` object can be overridden by
// `apollographql-client-*` headers set in `context.headers`.
if (name && !headers.has('apollographql-client-name')) {
headers = headers.set('apollographql-client-name', name);
}
if (version && !headers.has('apollographql-client-version')) {
headers = headers.set('apollographql-client-version', version);
}
}
return headers;
}
const defaults = {
batchInterval: 10,
batchMax: 10,
uri: 'graphql',
method: 'POST',
withCredentials: false,
includeQuery: true,
includeExtensions: false,
useMultipart: false,
};
/**
* Decides which value to pick from Context, Options or defaults
*/
function pick(context, options, key) {
return prioritize(context[key], options[key], defaults[key]);
}
class HttpBatchLinkHandler extends ApolloLink {
httpClient;
options;
batcher;
batchInterval;
batchMax;
print = print;
constructor(httpClient, options) {
super();
this.httpClient = httpClient;
this.options = options;
this.batchInterval = options.batchInterval || defaults.batchInterval;
this.batchMax = options.batchMax || defaults.batchMax;
if (this.options.operationPrinter) {
this.print = this.options.operationPrinter;
}
const batchHandler = (operations) => {
return new Observable$1((observer) => {
const body = this.createBody(operations);
const headers = this.createHeaders(operations);
const { method, uri, withCredentials } = this.createOptions(operations);
if (typeof uri === 'function') {
throw new Error(`Option 'uri' is a function, should be a string`);
}
const req = {
method,
url: uri,
body: body,
options: {
withCredentials,
headers,
},
};
const sub = fetch(req, this.httpClient, () => {
throw new Error('File upload is not available when combined with Batching');
}).subscribe({
next: result => observer.next(result.body),
error: err => observer.error(err),
complete: () => observer.complete(),
});
return () => {
if (!sub.closed) {
sub.unsubscribe();
}
};
});
};
const batchKey = options.batchKey ||
((operation) => {
return this.createBatchKey(operation);
});
this.batcher = new BatchLink({
batchInterval: this.batchInterval,
batchMax: this.batchMax,
batchKey,
batchHandler,
});
}
createOptions(operations) {
const context = operations[0].getContext();
return {
method: pick(context, this.options, 'method'),
uri: pick(context, this.options, 'uri'),
withCredentials: pick(context, this.options, 'withCredentials'),
};
}
createBody(operations) {
return operations.map(operation => {
const includeExtensions = prioritize(operation.getContext().includeExtensions, this.options.includeExtensions, false);
const includeQuery = prioritize(operation.getContext().includeQuery, this.options.includeQuery, true);
const body = {
operationName: operation.operationName,
variables: operation.variables,
};
if (includeExtensions) {
body.extensions = operation.extensions;
}
if (includeQuery) {
body.query = this.print(operation.query);
}
return body;
});
}
createHeaders(operations) {
return operations.reduce((headers, operation) => {
return mergeHeaders(headers, operation.getContext().headers);
}, createHeadersWithClientAwareness({
headers: this.options.headers,
clientAwareness: operations[0]?.getContext()?.clientAwareness,
}));
}
createBatchKey(operation) {
const context = operation.getContext();
if (context.skipBatching) {
return Math.random().toString(36).substring(2, 11);
}
const headers = context.headers && context.headers.keys().map((k) => context.headers.get(k));
const opts = JSON.stringify({
includeQuery: context.includeQuery,
includeExtensions: context.includeExtensions,
headers,
});
return prioritize(context.uri, this.options.uri, '') + opts;
}
request(op) {
return this.batcher.request(op);
}
}
class HttpBatchLink {
httpClient;
constructor(httpClient) {
this.httpClient = httpClient;
}
create(options) {
return new HttpBatchLinkHandler(this.httpClient, options);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.5", ngImport: i0, type: HttpBatchLink, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.0.5", ngImport: i0, type: HttpBatchLink, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.5", ngImport: i0, type: HttpBatchLink, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [{ type: i1.HttpClient }] });
// XXX find a better name for it
class HttpLinkHandler extends ApolloLink {
httpClient;
options;
requester;
print = print;
constructor(httpClient, options) {
super();
this.httpClient = httpClient;
this.options = options;
if (this.options.operationPrinter) {
this.print = this.options.operationPrinter;
}
this.requester = (operation) => new Observable$1((observer) => {
const context = operation.getContext();
let method = pick(context, this.options, 'method');
const includeQuery = pick(context, this.options, 'includeQuery');
const includeExtensions = pick(context, this.options, 'includeExtensions');
const url = pick(context, this.options, 'uri');
const withCredentials = pick(context, this.options, 'withCredentials');
const useMultipart = pick(context, this.options, 'useMultipart');
const useGETForQueries = this.options.useGETForQueries === true;
const isQuery = operation.query.definitions.some(def => def.kind === 'OperationDefinition' && def.operation === 'query');
if (useGETForQueries && isQuery) {
method = 'GET';
}
const req = {
method,
url: typeof url === 'function' ? url(operation) : url,
body: {
operationName: operation.operationName,
variables: operation.variables,
},
options: {
withCredentials,
useMultipart,
headers: this.options.headers,
},
};
if (includeExtensions) {
req.body.extensions = operation.extensions;
}
if (includeQuery) {
req.body.query = this.print(operation.query);
}
const headers = createHeadersWithClientAwareness(context);
req.options.headers = mergeHeaders(req.options.headers, headers);
const sub = fetch(req, this.httpClient, this.options.extractFiles).subscribe({
next: response => {
operation.setContext({ response });
observer.next(response.body);
},
error: err => observer.error(err),
complete: () => observer.complete(),
});
return () => {
if (!sub.closed) {
sub.unsubscribe();
}
};
});
}
request(op) {
return this.requester(op);
}
}
class HttpLink {
httpClient;
constructor(httpClient) {
this.httpClient = httpClient;
}
create(options) {
return new HttpLinkHandler(this.httpClient, options);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.5", ngImport: i0, type: HttpLink, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.0.5", ngImport: i0, type: HttpLink, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.5", ngImport: i0, type: HttpLink, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [{ type: i1.HttpClient }] });
// http
/**
* Generated bundle index. Do not edit.
*/
export { HttpBatchLink, HttpBatchLinkHandler, HttpLink, HttpLinkHandler };
//# sourceMappingURL=ngApolloLinkHttp.mjs.map