first commit

This commit is contained in:
2026-10-03 13:40:26 +02:00
parent 732a5b5a16
commit 566f415fa7
555 changed files with 62704 additions and 0 deletions

View 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';

View File

@@ -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;
}

View 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`,
};
}

View 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);
}

View 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' },
},
};
}),
]);
}