Skip to content

Commit

Permalink
feat: introduces accountFragment in client config
Browse files Browse the repository at this point in the history
  • Loading branch information
cesarenaldi committed Dec 10, 2024
1 parent 9dcd3b3 commit 31d8364
Show file tree
Hide file tree
Showing 10 changed files with 258 additions and 113 deletions.
13 changes: 8 additions & 5 deletions packages/client/src/actions/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import type {
import {
AccountFeedsStatsQuery,
AccountGraphsStatsQuery,
AccountQuery,
AccountStatsQuery,
AccountsAvailableQuery,
AccountsBlockedQuery,
Expand All @@ -35,10 +34,13 @@ import {
SearchAccountsQuery,
SetAccountMetadataMutation,
UnmuteAccountMutation,
accountQuery,
} from '@lens-protocol/graphql';
import type { ResultAsync } from '@lens-protocol/types';

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

Expand All @@ -57,11 +59,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
72 changes: 39 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 = 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,7 @@ 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> {
return new PublicClient(configureContext(options));
}

Expand All @@ -193,7 +185,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 +209,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 +236,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 +248,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 +293,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 +335,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 +464,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>),
};
}
8 changes: 0 additions & 8 deletions packages/client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,6 @@ export type OperationHandler<T extends string, E extends string> =
| RestrictedOperationHandler<E>
| DelegableOperationHandler<T, E>;

/**
* A standardized data object.
*
* All GQL operations should alias their results to `value` to ensure interoperability
* with this client interface.
*/
export type StandardData<T> = { value: T };

/**
* A paginated list of items.
*/
Expand Down
11 changes: 5 additions & 6 deletions packages/graphql/src/accounts/account.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
import type { FragmentOf } from 'gql.tada';
import {
Account,
AccountAvailable,
AccountBlocked,
AccountFragment,
PaginatedResultInfo,
SelfFundedTransactionRequest,
SponsoredTransactionRequest,
TransactionWillFail,
} from '../fragments';
import { type RequestOf, graphql } from '../graphql';
import { type RequestOf, type RequestOfFactory, factory, graphql } from '../graphql';

export const AccountQuery = graphql(
export const accountQuery = factory(
`query Account($request: AccountRequest!) {
value: account(request: $request) {
...Account
}
}`,
[Account],
);

export type AccountRequest = RequestOf<typeof AccountQuery>;
export type AccountRequest = RequestOfFactory<typeof accountQuery>;

export const SearchAccountsQuery = graphql(
`query SearchAccounts($request: AccountSearchRequest!) {
Expand All @@ -33,7 +32,7 @@ export const SearchAccountsQuery = graphql(
}
}
}`,
[Account, PaginatedResultInfo],
[AccountFragment, PaginatedResultInfo],
);

export type SearchAccountsRequest = RequestOf<typeof SearchAccountsQuery>;
Expand Down
9 changes: 5 additions & 4 deletions packages/graphql/src/fragments/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const AccountMetadata = graphql(
);
export type AccountMetadata = FragmentOf<typeof AccountMetadata>;

export const Account = graphql(
export const AccountFragment = graphql(
`fragment Account on Account {
__typename
address
Expand All @@ -38,11 +38,12 @@ export const Account = graphql(
}`,
[Username],
);
export type Account = FragmentOf<typeof Account>;
export type Account = FragmentOf<typeof AccountFragment>;

export const FullAccount = graphql(
export const FullAccountFragment = graphql(
`fragment Account on Account {
__typename
full: address
address
score
metadata {
Expand All @@ -57,7 +58,7 @@ export const FullAccount = graphql(
}`,
[AccountMetadata, LoggedInAccountOperations, Username],
);
export type FullAccount = FragmentOf<typeof FullAccount>;
export type FullAccount = FragmentOf<typeof FullAccountFragment>;

const AccountManagerPermissions = graphql(
`fragment AccountManagerPermissions on AccountManagerPermissions {
Expand Down
Loading

0 comments on commit 31d8364

Please sign in to comment.