Skip to content

Commit

Permalink
feat: enables custom AccountFragment
Browse files Browse the repository at this point in the history
- fix: type-fest dep
- feat: adds test coverage before refactoring
- fix: client generic type and extra aliased field
- feat: renames Post fragment into PostFragment
- feat: renames PaginatedResultInfo into PaginatedResultInfoFragment
- feat: injects AccountFragment into notifications query
  • Loading branch information
cesarenaldi committed Dec 12, 2024
1 parent c6dcb3e commit 8a0c40b
Show file tree
Hide file tree
Showing 25 changed files with 408 additions and 217 deletions.
25 changes: 25 additions & 0 deletions packages/client/src/actions/account.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { testnet } from '@lens-protocol/env';
import { assertOk, evmAddress } from '@lens-protocol/types';
import { describe, it } from 'vitest';

import { FullAccountFragment } from '@lens-protocol/graphql';
import { PublicClient } from '../clients';
import { fetchAccount } from './account';

describe('Given the Account query actions', () => {
const client = PublicClient.create({
environment: testnet,
origin: 'http://example.com',
accountFragment: FullAccountFragment,
});

describe(`When invoking the '${fetchAccount.name}' action`, () => {
it('Then it should not fail w/ a GQL BadRequest error', async () => {
const result = await fetchAccount(client, {
address: evmAddress(import.meta.env.TEST_ACCOUNT),
});

assertOk(result);
});
});
});
12 changes: 7 additions & 5 deletions packages/client/src/actions/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import type {
import {
AccountFeedsStatsQuery,
AccountGraphsStatsQuery,
AccountQuery,
AccountStatsQuery,
AccountsAvailableQuery,
AccountsBlockedQuery,
Expand All @@ -47,10 +46,12 @@ import {
UnblockMutation,
UndoRecommendAccountMutation,
UnmuteAccountMutation,
accountQuery,
} from '@lens-protocol/graphql';
import type { ResultAsync } from '@lens-protocol/types';

import type { AnyClient, SessionClient } from '../clients';
import type { Context } from '../context';
import type { UnauthenticatedError, UnexpectedError } from '../errors';
import type { Paginated } from '../types';

Expand All @@ -69,11 +70,12 @@ import type { Paginated } from '../types';
* @param request - The Account query request.
* @returns The Account or `null` if it does not exist.
*/
export function fetchAccount(
client: AnyClient,
export function fetchAccount<TAccount extends Account>(
client: AnyClient<Context<TAccount>>,
request: AccountRequest,
): ResultAsync<Account | null, UnexpectedError> {
return client.query(AccountQuery, { request });
): ResultAsync<TAccount | null, UnexpectedError> {
const document = accountQuery(client.context.accountFragment);
return client.query(document, { request });
}

/**
Expand Down
15 changes: 15 additions & 0 deletions packages/client/src/actions/notifications.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { assertOk } from '@lens-protocol/types';
import { describe, it } from 'vitest';

import { loginAsAccountOwner } from '../../testing-utils';
import { fetchNotifications } from './notifications';

describe(`Given the '${fetchNotifications.name}' action`, () => {
describe('When invoked', () => {
it('Then it should not fail w/ a GQL BadRequest error', async () => {
const result = await loginAsAccountOwner().andThen(fetchNotifications);

assertOk(result);
});
});
});
15 changes: 9 additions & 6 deletions packages/client/src/actions/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { Notification, NotificationsRequest } from '@lens-protocol/graphql';
import { NotificationsQuery } from '@lens-protocol/graphql';
import { notificationsQuery } from '@lens-protocol/graphql';
import type { ResultAsync } from '@lens-protocol/types';

import type { Account } from '@lens-protocol/graphql';
import type { SessionClient } from '../clients';
import type { Context } from '../context';
import type { UnexpectedError } from '../errors';
import type { Paginated } from '../types';

Expand All @@ -17,9 +19,10 @@ import type { Paginated } from '../types';
* @param request - The query request.
* @returns Paginated notifications.
*/
export function fetchNotifications(
client: SessionClient,
request: NotificationsRequest,
): ResultAsync<Paginated<Notification>, UnexpectedError> {
return client.query(NotificationsQuery, { request });
export function fetchNotifications<TAccount extends Account>(
client: SessionClient<Context<TAccount>>,
request: NotificationsRequest = {},
): ResultAsync<Paginated<Notification<TAccount>>, UnexpectedError> {
const document = notificationsQuery(client.context.accountFragment);
return client.query(document, { request });
}
1 change: 0 additions & 1 deletion packages/client/src/actions/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type {
AccountPostReaction,
ActionInfo,
AnyPost,
Post,
PostActionsRequest,
PostBookmarksRequest,
PostReactionsRequest,
Expand Down
74 changes: 41 additions & 33 deletions packages/client/src/clients.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { EnvironmentConfig } from '@lens-protocol/env';
import { AuthenticateMutation, ChallengeMutation } from '@lens-protocol/graphql';
import type {
AuthenticationChallenge,
ChallengeRequest,
SignedAuthChallenge,
StandardData,
} from '@lens-protocol/graphql';
import type { Credentials, IStorage, IStorageProvider } from '@lens-protocol/storage';
import type { Credentials, IStorage } from '@lens-protocol/storage';
import { createCredentialsStorage } from '@lens-protocol/storage';
import {
ResultAsync,
Expand All @@ -29,10 +29,11 @@ import {
} from '@urql/core';
import { type Logger, getLogger } from 'loglevel';

import type { Account } from '@lens-protocol/graphql';
import { type AuthenticatedUser, authenticatedUser } from './AuthenticatedUser';
import { transactionStatus } from './actions';
import type { ClientConfig } from './config';
import { configureContext } from './context';
import { type Context, configureContext } from './context';
import {
AuthenticationError,
GraphQLErrorCode,
Expand All @@ -43,7 +44,6 @@ import {
hasExtensionCode,
} from './errors';
import { decodeIdToken } from './tokens';
import type { StandardData } from './types';
import { delay } from './utils';

function takeValue<T>({
Expand All @@ -54,31 +54,20 @@ function takeValue<T>({
return data.value;
}

/**
* @internal
*/
type ClientContext = {
environment: EnvironmentConfig;
cache: boolean;
debug: boolean;
origin?: string;
storage: IStorageProvider;
};

export type SignMessage = (message: string) => Promise<string>;

export type LoginParams = ChallengeRequest & {
signMessage: SignMessage;
};

abstract class AbstractClient<TError> {
abstract class AbstractClient<TContext extends Context, TError> {
protected readonly urql: UrqlClient;

protected readonly logger: Logger;

protected readonly credentials: IStorage<Credentials>;

protected constructor(public readonly context: ClientContext) {
protected constructor(public readonly context: TContext) {
this.credentials = createCredentialsStorage(context.storage, context.environment.name);

this.logger = getLogger(this.constructor.name);
Expand Down Expand Up @@ -111,12 +100,12 @@ abstract class AbstractClient<TError> {
/**
* Asserts that the client is a {@link PublicClient}.
*/
public abstract isPublicClient(): this is PublicClient;
public abstract isPublicClient(): this is PublicClient<TContext>;

/**
* that the client is a {@link SessionClient}.
*/
public abstract isSessionClient(): this is SessionClient;
public abstract isSessionClient(): this is SessionClient<TContext>;

public abstract query<TValue, TVariables extends AnyVariables>(
document: TypedDocumentNode<StandardData<TValue>, TVariables>,
Expand Down Expand Up @@ -156,13 +145,16 @@ abstract class AbstractClient<TError> {
/**
* A client to interact with the public access queries and mutations of the Lens GraphQL API.
*/
export class PublicClient extends AbstractClient<UnexpectedError> {
export class PublicClient<TContext extends Context> extends AbstractClient<
TContext,
UnexpectedError
> {
/**
* The current session client.
*
* This could be the {@link PublicClient} itself if the user is not authenticated, or a {@link SessionClient} if the user is authenticated.
*/
public currentSession: PublicClient | SessionClient = this;
public currentSession: PublicClient<TContext> | SessionClient<TContext> = this;

/**
* Create a new instance of the {@link PublicClient}.
Expand All @@ -177,7 +169,9 @@ export class PublicClient extends AbstractClient<UnexpectedError> {
* @param options - The options to configure the client.
* @returns The new instance of the client.
*/
static create(options: ClientConfig): PublicClient {
static create<TAccount extends Account>(
options: ClientConfig<TAccount>,
): PublicClient<Context<TAccount>> {
return new PublicClient(configureContext(options));
}

Expand All @@ -193,7 +187,7 @@ export class PublicClient extends AbstractClient<UnexpectedError> {
*/
authenticate(
request: SignedAuthChallenge,
): ResultAsync<SessionClient, AuthenticationError | UnexpectedError> {
): ResultAsync<SessionClient<TContext>, AuthenticationError | UnexpectedError> {
return this.mutation(AuthenticateMutation, { request })
.andThen((result) => {
if (result.__typename === 'AuthenticationTokens') {
Expand All @@ -217,7 +211,7 @@ export class PublicClient extends AbstractClient<UnexpectedError> {
signMessage,
...request
}: LoginParams): ResultAsync<
SessionClient,
SessionClient<TContext>,
AuthenticationError | SigningError | UnexpectedError
> {
return this.challenge(request)
Expand All @@ -244,7 +238,7 @@ export class PublicClient extends AbstractClient<UnexpectedError> {
*
* @returns The session client if available.
*/
resumeSession(): ResultAsync<SessionClient, UnauthenticatedError> {
resumeSession(): ResultAsync<SessionClient<TContext>, UnauthenticatedError> {
return ResultAsync.fromSafePromise(this.credentials.get()).andThen((credentials) => {
if (!credentials) {
return new UnauthenticatedError('No credentials found').asResultAsync();
Expand All @@ -256,14 +250,14 @@ export class PublicClient extends AbstractClient<UnexpectedError> {
/**
* {@inheritDoc AbstractClient.isPublicClient}
*/
public override isPublicClient(): this is PublicClient {
public override isPublicClient(): this is PublicClient<TContext> {
return true;
}

/**
* {@inheritDoc AbstractClient.isSessionClient}
*/
public override isSessionClient(): this is SessionClient {
public override isSessionClient(): this is SessionClient<TContext> {
return false;
}

Expand Down Expand Up @@ -301,12 +295,15 @@ export class PublicClient extends AbstractClient<UnexpectedError> {
*
* @privateRemarks Intentionally not exported.
*/
class SessionClient extends AbstractClient<UnauthenticatedError | UnexpectedError> {
public get parent(): PublicClient {
class SessionClient<TContext extends Context = Context> extends AbstractClient<
TContext,
UnauthenticatedError | UnexpectedError
> {
public get parent(): PublicClient<TContext> {
return this._parent;
}

constructor(private readonly _parent: PublicClient) {
constructor(private readonly _parent: PublicClient<TContext>) {
super(_parent.context);
_parent.currentSession = this;
}
Expand Down Expand Up @@ -340,14 +337,14 @@ class SessionClient extends AbstractClient<UnauthenticatedError | UnexpectedErro
/**
* {@inheritDoc AbstractClient.isPublicClient}
*/
public override isPublicClient(): this is PublicClient {
public override isPublicClient(): this is PublicClient<TContext> {
return false;
}

/**
* {@inheritDoc AbstractClient.isSessionClient}
*/
public override isSessionClient(): this is SessionClient {
public override isSessionClient(): this is SessionClient<TContext> {
return true;
}

Expand Down Expand Up @@ -469,4 +466,15 @@ export type { SessionClient };
/**
* Any client that can be used to interact with the Lens GraphQL API.
*/
export type AnyClient = PublicClient | SessionClient;
// TODO remove default
export type AnyClient<TContext extends Context = Context> =
| PublicClient<TContext>
| SessionClient<TContext>;

export type AccountFromContext<TContext extends Context> = TContext extends Context<infer TAccount>
? TAccount
: never;

export type AccountFromClient<TClient extends AnyClient> = TClient extends AnyClient<infer TContext>
? AccountFromContext<TContext>
: never;
9 changes: 8 additions & 1 deletion packages/client/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { EnvironmentConfig } from '@lens-protocol/env';
import type { Account, AccountFragment, FragmentDocumentFor } from '@lens-protocol/graphql';
import type { IStorageProvider } from '@lens-protocol/storage';

/**
* The client configuration.
*/
export type ClientConfig = {
export type ClientConfig<TAccount extends Account> = {
/**
* The environment configuration to use (e.g. `mainnet`, `testnet`).
*/
Expand Down Expand Up @@ -34,4 +35,10 @@ export type ClientConfig = {
* @defaultValue {@link InMemoryStorageProvider}
*/
storage?: IStorageProvider;
/**
* The Account Fragment to use.
*
* @defaultValue {@link AccountFragment}
*/
accountFragment?: FragmentDocumentFor<TAccount>;
};
10 changes: 8 additions & 2 deletions packages/client/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import type { EnvironmentConfig } from '@lens-protocol/env';
import { AccountFragment } from '@lens-protocol/graphql';
import type { Account, FragmentDocumentFor } from '@lens-protocol/graphql';
import { type IStorageProvider, InMemoryStorageProvider } from '@lens-protocol/storage';
import type { ClientConfig } from './config';

/**
* @internal
*/
export type Context = {
export type Context<TAccount extends Account = Account> = {
environment: EnvironmentConfig;
cache: boolean;
debug: boolean;
origin?: string;
storage: IStorageProvider;
accountFragment: FragmentDocumentFor<TAccount>;
};

/**
* @internal
*/
export function configureContext(from: ClientConfig): Context {
export function configureContext<TAccount extends Account>(
from: ClientConfig<TAccount>,
): Context<TAccount> {
return {
environment: from.environment,
cache: from.cache ?? false,
debug: from.debug ?? false,
origin: from.origin,
storage: from.storage ?? new InMemoryStorageProvider(),
accountFragment: from.accountFragment ?? (AccountFragment as FragmentDocumentFor<TAccount>),
};
}
Loading

0 comments on commit 8a0c40b

Please sign in to comment.