Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduces accountFragment in client config #1008

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be decided once we have rolled out the solution to all relevant queries. It might stay.

| 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
Loading