first commit
This commit is contained in:
4
libs/web/shared/infrastructure/src/index.ts
Normal file
4
libs/web/shared/infrastructure/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './lib/api-config';
|
||||
export * from './lib/access-token-source';
|
||||
export * from './lib/graphql.provider';
|
||||
export * from './lib/graphql-errors';
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Port used by the GraphQL client to authenticate requests. Implemented by
|
||||
* the identity context (which knows how to refresh and sign out).
|
||||
*/
|
||||
export abstract class AccessTokenSource {
|
||||
/** A valid access token (refreshed if needed), or null when signed out. */
|
||||
abstract accessToken(): Promise<string | null>;
|
||||
/** The API rejected the token: the session is over. */
|
||||
abstract unauthenticated(): void;
|
||||
}
|
||||
32
libs/web/shared/infrastructure/src/lib/api-config.ts
Normal file
32
libs/web/shared/infrastructure/src/lib/api-config.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
InjectionToken,
|
||||
makeEnvironmentProviders,
|
||||
type EnvironmentProviders,
|
||||
} from '@angular/core';
|
||||
|
||||
export interface ApiConfig {
|
||||
/** Base of the REST endpoints, e.g. `/api`. */
|
||||
restUrl: string;
|
||||
/** GraphQL over HTTP, e.g. `/graphql`. */
|
||||
graphqlUrl: string;
|
||||
/** GraphQL subscriptions (graphql-ws), e.g. `ws://localhost:4200/graphql`. */
|
||||
graphqlWsUrl: string;
|
||||
}
|
||||
|
||||
export const API_CONFIG = new InjectionToken<ApiConfig>('API_CONFIG');
|
||||
|
||||
export function provideApiConfig(config: ApiConfig): EnvironmentProviders {
|
||||
return makeEnvironmentProviders([{ provide: API_CONFIG, useValue: config }]);
|
||||
}
|
||||
|
||||
/** Relative URLs resolved against the page, so the dev proxy / reverse proxy can serve the API. */
|
||||
export function apiConfigFromLocation(
|
||||
location: Location = window.location,
|
||||
): ApiConfig {
|
||||
const ws = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return {
|
||||
restUrl: '/api',
|
||||
graphqlUrl: '/graphql',
|
||||
graphqlWsUrl: `${ws}//${location.host}/graphql`,
|
||||
};
|
||||
}
|
||||
10
libs/web/shared/infrastructure/src/lib/graphql-errors.ts
Normal file
10
libs/web/shared/infrastructure/src/lib/graphql-errors.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
|
||||
/** A message fit for a toast from whatever the GraphQL client threw. */
|
||||
export function errorMessage(error: unknown): string {
|
||||
if (CombinedGraphQLErrors.is(error)) {
|
||||
return error.errors.map((e) => e.message).join('\n');
|
||||
}
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
}
|
||||
95
libs/web/shared/infrastructure/src/lib/graphql.provider.ts
Normal file
95
libs/web/shared/infrastructure/src/lib/graphql.provider.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
inject,
|
||||
makeEnvironmentProviders,
|
||||
type EnvironmentProviders,
|
||||
} from '@angular/core';
|
||||
import { ApolloLink, InMemoryCache } from '@apollo/client';
|
||||
import { SetContextLink } from '@apollo/client/link/context';
|
||||
import { ErrorLink } from '@apollo/client/link/error';
|
||||
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
|
||||
import { getMainDefinition } from '@apollo/client/utilities';
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
import { provideApollo } from 'apollo-angular';
|
||||
import { HttpLink } from 'apollo-angular/http';
|
||||
import { createClient } from 'graphql-ws';
|
||||
import { AccessTokenSource } from './access-token-source';
|
||||
import { API_CONFIG } from './api-config';
|
||||
|
||||
/**
|
||||
* Apollo client for the task-stream API: HTTP for queries/mutations,
|
||||
* graphql-ws for subscriptions (token in `connection_init`).
|
||||
*
|
||||
* The cache is disabled on purpose: state lives in the NgRx signal stores,
|
||||
* which are fed by queries and kept fresh by subscriptions.
|
||||
*/
|
||||
export function provideGraphql(): EnvironmentProviders {
|
||||
return makeEnvironmentProviders([
|
||||
provideApollo(() => {
|
||||
const config = inject(API_CONFIG);
|
||||
const tokens = inject(AccessTokenSource);
|
||||
const httpLink = inject(HttpLink);
|
||||
|
||||
const auth = new SetContextLink(async (prevContext) => {
|
||||
const token = await tokens.accessToken();
|
||||
return token
|
||||
? {
|
||||
headers: {
|
||||
...prevContext['headers'],
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
});
|
||||
|
||||
const errors = new ErrorLink(({ error }) => {
|
||||
if (
|
||||
CombinedGraphQLErrors.is(error) &&
|
||||
error.errors.some((e) => e.extensions?.['code'] === 'UNAUTHENTICATED')
|
||||
) {
|
||||
tokens.unauthenticated();
|
||||
}
|
||||
});
|
||||
|
||||
const ws = new GraphQLWsLink(
|
||||
createClient({
|
||||
url: config.graphqlWsUrl,
|
||||
lazy: true,
|
||||
retryAttempts: Infinity,
|
||||
shouldRetry: () => true,
|
||||
connectionParams: async () => {
|
||||
const token = await tokens.accessToken();
|
||||
return token ? { authorization: `Bearer ${token}` } : {};
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const http = ApolloLink.from([
|
||||
auth,
|
||||
errors,
|
||||
httpLink.create({ uri: config.graphqlUrl }),
|
||||
]);
|
||||
|
||||
const link = ApolloLink.split(
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query);
|
||||
return (
|
||||
definition.kind === 'OperationDefinition' &&
|
||||
definition.operation === 'subscription'
|
||||
);
|
||||
},
|
||||
ApolloLink.from([errors, ws]),
|
||||
http,
|
||||
);
|
||||
|
||||
return {
|
||||
link,
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
query: { fetchPolicy: 'no-cache' },
|
||||
watchQuery: { fetchPolicy: 'no-cache' },
|
||||
mutate: { fetchPolicy: 'no-cache' },
|
||||
},
|
||||
};
|
||||
}),
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user