From 8a0c40bf1609d1c19b93ae1440fdcc961bf77b8f Mon Sep 17 00:00:00 2001 From: Cesare Naldi Date: Tue, 10 Dec 2024 14:10:06 +0100 Subject: [PATCH] feat: enables custom AccountFragment - 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 --- packages/client/src/actions/account.test.ts | 25 ++++ packages/client/src/actions/account.ts | 12 +- .../client/src/actions/notifications.test.ts | 15 ++ packages/client/src/actions/notifications.ts | 15 +- packages/client/src/actions/posts.ts | 1 - packages/client/src/clients.ts | 74 +++++---- packages/client/src/config.ts | 9 +- packages/client/src/context.ts | 10 +- packages/client/src/types.ts | 8 - packages/client/testing-utils.ts | 25 ++++ packages/graphql/src/accounts/account.ts | 37 +++-- packages/graphql/src/accounts/managers.ts | 4 +- packages/graphql/src/authentication.ts | 4 +- packages/graphql/src/follow.ts | 14 +- packages/graphql/src/fragments/account.ts | 14 +- packages/graphql/src/fragments/pagination.ts | 4 +- packages/graphql/src/fragments/post.ts | 18 +-- packages/graphql/src/graphql.ts | 138 ++++++++++++----- packages/graphql/src/notifications.ts | 141 ++++++++++-------- packages/graphql/src/post.ts | 10 +- packages/graphql/src/timeline.ts | 8 +- packages/react/src/provider.tsx | 7 +- packages/types/package.json | 4 +- packages/types/src/helpers/assertions.ts | 10 +- pnpm-lock.yaml | 18 +-- 25 files changed, 408 insertions(+), 217 deletions(-) create mode 100644 packages/client/src/actions/account.test.ts create mode 100644 packages/client/src/actions/notifications.test.ts create mode 100644 packages/client/testing-utils.ts diff --git a/packages/client/src/actions/account.test.ts b/packages/client/src/actions/account.test.ts new file mode 100644 index 000000000..3ed73696d --- /dev/null +++ b/packages/client/src/actions/account.test.ts @@ -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); + }); + }); +}); diff --git a/packages/client/src/actions/account.ts b/packages/client/src/actions/account.ts index ec5f1f199..b1e8b319b 100644 --- a/packages/client/src/actions/account.ts +++ b/packages/client/src/actions/account.ts @@ -31,7 +31,6 @@ import type { import { AccountFeedsStatsQuery, AccountGraphsStatsQuery, - AccountQuery, AccountStatsQuery, AccountsAvailableQuery, AccountsBlockedQuery, @@ -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'; @@ -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( + client: AnyClient>, request: AccountRequest, -): ResultAsync { - return client.query(AccountQuery, { request }); +): ResultAsync { + const document = accountQuery(client.context.accountFragment); + return client.query(document, { request }); } /** diff --git a/packages/client/src/actions/notifications.test.ts b/packages/client/src/actions/notifications.test.ts new file mode 100644 index 000000000..81a00a6f9 --- /dev/null +++ b/packages/client/src/actions/notifications.test.ts @@ -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); + }); + }); +}); diff --git a/packages/client/src/actions/notifications.ts b/packages/client/src/actions/notifications.ts index 4ec4c0822..0e013969c 100644 --- a/packages/client/src/actions/notifications.ts +++ b/packages/client/src/actions/notifications.ts @@ -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'; @@ -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, UnexpectedError> { - return client.query(NotificationsQuery, { request }); +export function fetchNotifications( + client: SessionClient>, + request: NotificationsRequest = {}, +): ResultAsync>, UnexpectedError> { + const document = notificationsQuery(client.context.accountFragment); + return client.query(document, { request }); } diff --git a/packages/client/src/actions/posts.ts b/packages/client/src/actions/posts.ts index bf9426d2f..07fbf6530 100644 --- a/packages/client/src/actions/posts.ts +++ b/packages/client/src/actions/posts.ts @@ -2,7 +2,6 @@ import type { AccountPostReaction, ActionInfo, AnyPost, - Post, PostActionsRequest, PostBookmarksRequest, PostReactionsRequest, diff --git a/packages/client/src/clients.ts b/packages/client/src/clients.ts index 1082d98a1..42622dc9c 100644 --- a/packages/client/src/clients.ts +++ b/packages/client/src/clients.ts @@ -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, @@ -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, @@ -43,7 +44,6 @@ import { hasExtensionCode, } from './errors'; import { decodeIdToken } from './tokens'; -import type { StandardData } from './types'; import { delay } from './utils'; function takeValue({ @@ -54,31 +54,20 @@ function takeValue({ return data.value; } -/** - * @internal - */ -type ClientContext = { - environment: EnvironmentConfig; - cache: boolean; - debug: boolean; - origin?: string; - storage: IStorageProvider; -}; - export type SignMessage = (message: string) => Promise; export type LoginParams = ChallengeRequest & { signMessage: SignMessage; }; -abstract class AbstractClient { +abstract class AbstractClient { protected readonly urql: UrqlClient; protected readonly logger: Logger; protected readonly credentials: IStorage; - 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); @@ -111,12 +100,12 @@ abstract class AbstractClient { /** * Asserts that the client is a {@link PublicClient}. */ - public abstract isPublicClient(): this is PublicClient; + public abstract isPublicClient(): this is PublicClient; /** * that the client is a {@link SessionClient}. */ - public abstract isSessionClient(): this is SessionClient; + public abstract isSessionClient(): this is SessionClient; public abstract query( document: TypedDocumentNode, TVariables>, @@ -156,13 +145,16 @@ abstract class AbstractClient { /** * A client to interact with the public access queries and mutations of the Lens GraphQL API. */ -export class PublicClient extends AbstractClient { +export class PublicClient 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 | SessionClient = this; /** * Create a new instance of the {@link PublicClient}. @@ -177,7 +169,9 @@ export class PublicClient extends AbstractClient { * @param options - The options to configure the client. * @returns The new instance of the client. */ - static create(options: ClientConfig): PublicClient { + static create( + options: ClientConfig, + ): PublicClient> { return new PublicClient(configureContext(options)); } @@ -193,7 +187,7 @@ export class PublicClient extends AbstractClient { */ authenticate( request: SignedAuthChallenge, - ): ResultAsync { + ): ResultAsync, AuthenticationError | UnexpectedError> { return this.mutation(AuthenticateMutation, { request }) .andThen((result) => { if (result.__typename === 'AuthenticationTokens') { @@ -217,7 +211,7 @@ export class PublicClient extends AbstractClient { signMessage, ...request }: LoginParams): ResultAsync< - SessionClient, + SessionClient, AuthenticationError | SigningError | UnexpectedError > { return this.challenge(request) @@ -244,7 +238,7 @@ export class PublicClient extends AbstractClient { * * @returns The session client if available. */ - resumeSession(): ResultAsync { + resumeSession(): ResultAsync, UnauthenticatedError> { return ResultAsync.fromSafePromise(this.credentials.get()).andThen((credentials) => { if (!credentials) { return new UnauthenticatedError('No credentials found').asResultAsync(); @@ -256,14 +250,14 @@ export class PublicClient extends AbstractClient { /** * {@inheritDoc AbstractClient.isPublicClient} */ - public override isPublicClient(): this is PublicClient { + public override isPublicClient(): this is PublicClient { return true; } /** * {@inheritDoc AbstractClient.isSessionClient} */ - public override isSessionClient(): this is SessionClient { + public override isSessionClient(): this is SessionClient { return false; } @@ -301,12 +295,15 @@ export class PublicClient extends AbstractClient { * * @privateRemarks Intentionally not exported. */ -class SessionClient extends AbstractClient { - public get parent(): PublicClient { +class SessionClient extends AbstractClient< + TContext, + UnauthenticatedError | UnexpectedError +> { + public get parent(): PublicClient { return this._parent; } - constructor(private readonly _parent: PublicClient) { + constructor(private readonly _parent: PublicClient) { super(_parent.context); _parent.currentSession = this; } @@ -340,14 +337,14 @@ class SessionClient extends AbstractClient { return false; } /** * {@inheritDoc AbstractClient.isSessionClient} */ - public override isSessionClient(): this is SessionClient { + public override isSessionClient(): this is SessionClient { return true; } @@ -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 = + | PublicClient + | SessionClient; + +export type AccountFromContext = TContext extends Context + ? TAccount + : never; + +export type AccountFromClient = TClient extends AnyClient + ? AccountFromContext + : never; diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts index 2e743c545..b91c75724 100644 --- a/packages/client/src/config.ts +++ b/packages/client/src/config.ts @@ -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 = { /** * The environment configuration to use (e.g. `mainnet`, `testnet`). */ @@ -34,4 +35,10 @@ export type ClientConfig = { * @defaultValue {@link InMemoryStorageProvider} */ storage?: IStorageProvider; + /** + * The Account Fragment to use. + * + * @defaultValue {@link AccountFragment} + */ + accountFragment?: FragmentDocumentFor; }; diff --git a/packages/client/src/context.ts b/packages/client/src/context.ts index 995d799f3..289ee53ac 100644 --- a/packages/client/src/context.ts +++ b/packages/client/src/context.ts @@ -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 = { environment: EnvironmentConfig; cache: boolean; debug: boolean; origin?: string; storage: IStorageProvider; + accountFragment: FragmentDocumentFor; }; /** * @internal */ -export function configureContext(from: ClientConfig): Context { +export function configureContext( + from: ClientConfig, +): Context { 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), }; } diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index 1daa950eb..df450c0b2 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -57,14 +57,6 @@ export type OperationHandler = | RestrictedOperationHandler | DelegableOperationHandler; -/** - * A standardized data object. - * - * All GQL operations should alias their results to `value` to ensure interoperability - * with this client interface. - */ -export type StandardData = { value: T }; - /** * A paginated list of items. */ diff --git a/packages/client/testing-utils.ts b/packages/client/testing-utils.ts new file mode 100644 index 000000000..44814a7b6 --- /dev/null +++ b/packages/client/testing-utils.ts @@ -0,0 +1,25 @@ +import { evmAddress } from '@lens-protocol/types'; +import { privateKeyToAccount } from 'viem/accounts'; +import { FullAccountFragment, PublicClient, testnet } from './src'; + +const signer = privateKeyToAccount(import.meta.env.PRIVATE_KEY); +const owner = evmAddress(signer.address); +const account = evmAddress(import.meta.env.TEST_ACCOUNT); +const app = evmAddress(import.meta.env.TEST_APP); + +export function loginAsAccountOwner() { + const client = PublicClient.create({ + environment: testnet, + origin: 'http://example.com', + accountFragment: FullAccountFragment, + }); + + return client.login({ + accountOwner: { + account, + owner, + app, + }, + signMessage: (message) => signer.signMessage({ message }), + }); +} diff --git a/packages/graphql/src/accounts/account.ts b/packages/graphql/src/accounts/account.ts index ad9a74538..447c31f33 100644 --- a/packages/graphql/src/accounts/account.ts +++ b/packages/graphql/src/accounts/account.ts @@ -1,25 +1,36 @@ import type { FragmentOf } from 'gql.tada'; import { - Account, + type Account, AccountAvailable, AccountBlocked, - PaginatedResultInfo, + AccountFragment, + PaginatedResultInfoFragment, SelfFundedTransactionRequest, SponsoredTransactionRequest, TransactionWillFail, } from '../fragments'; -import { type RequestOf, graphql } from '../graphql'; - -export const AccountQuery = graphql( - `query Account($request: AccountRequest!) { +import { + type FragmentDocumentFor, + type RequestFrom, + type RequestOf, + type StandardDocumentNode, + graphql, +} from '../graphql'; + +const AccountQueryString = ` + query Account($request: AccountRequest!) { value: account(request: $request) { ...Account } - }`, - [Account], -); + } +`; +export type AccountRequest = RequestFrom; -export type AccountRequest = RequestOf; +export function accountQuery( + fragment: FragmentDocumentFor, +): StandardDocumentNode { + return graphql(AccountQueryString, [fragment]) as StandardDocumentNode; +} export const SearchAccountsQuery = graphql( `query SearchAccounts($request: AccountSearchRequest!) { @@ -33,7 +44,7 @@ export const SearchAccountsQuery = graphql( } } }`, - [Account, PaginatedResultInfo], + [AccountFragment, PaginatedResultInfoFragment], ); export type SearchAccountsRequest = RequestOf; @@ -210,7 +221,7 @@ export const AccountsAvailableQuery = graphql( } } }`, - [AccountAvailable, PaginatedResultInfo], + [AccountAvailable, PaginatedResultInfoFragment], ); export type AccountsAvailableRequest = RequestOf; @@ -226,7 +237,7 @@ export const AccountsBlockedQuery = graphql( } } }`, - [AccountBlocked, PaginatedResultInfo], + [AccountBlocked, PaginatedResultInfoFragment], ); export type AccountsBlockedRequest = RequestOf; diff --git a/packages/graphql/src/accounts/managers.ts b/packages/graphql/src/accounts/managers.ts index 319ad1bdc..35026f1d6 100644 --- a/packages/graphql/src/accounts/managers.ts +++ b/packages/graphql/src/accounts/managers.ts @@ -1,7 +1,7 @@ import type { FragmentOf } from 'gql.tada'; import { AccountManager, - PaginatedResultInfo, + PaginatedResultInfoFragment, SelfFundedTransactionRequest, SponsoredTransactionRequest, TransactionWillFail, @@ -19,7 +19,7 @@ export const AccountManagersQuery = graphql( } } }`, - [AccountManager, PaginatedResultInfo], + [AccountManager, PaginatedResultInfoFragment], ); export type AccountManagersRequest = RequestOf; diff --git a/packages/graphql/src/authentication.ts b/packages/graphql/src/authentication.ts index 32b588fe1..07ff8b79a 100644 --- a/packages/graphql/src/authentication.ts +++ b/packages/graphql/src/authentication.ts @@ -1,5 +1,5 @@ import type { FragmentOf } from 'gql.tada'; -import { PaginatedResultInfo } from './fragments'; +import { PaginatedResultInfoFragment } from './fragments'; import { type RequestOf, graphql } from './graphql'; const AuthenticationChallenge = graphql( @@ -124,7 +124,7 @@ export const AuthenticatedSessionsQuery = graphql( } } }`, - [AuthenticatedSession, PaginatedResultInfo], + [AuthenticatedSession, PaginatedResultInfoFragment], ); export type AuthenticatedSessionsRequest = RequestOf; diff --git a/packages/graphql/src/follow.ts b/packages/graphql/src/follow.ts index 22f6c88de..5a3abe83c 100644 --- a/packages/graphql/src/follow.ts +++ b/packages/graphql/src/follow.ts @@ -1,9 +1,9 @@ import type { FragmentOf } from 'gql.tada'; import { - Account, + AccountFragment, BooleanValue, - PaginatedResultInfo, + PaginatedResultInfoFragment, SelfFundedTransactionRequest, SponsoredTransactionRequest, TransactionWillFail, @@ -97,7 +97,7 @@ const Follower = graphql( } followedOn }`, - [Account], + [AccountFragment], ); export type Follower = FragmentOf; @@ -109,7 +109,7 @@ const Following = graphql( } followedOn }`, - [Account], + [AccountFragment], ); export type Following = FragmentOf; @@ -125,7 +125,7 @@ export const FollowersQuery = graphql( } } }`, - [Follower, PaginatedResultInfo], + [Follower, PaginatedResultInfoFragment], ); export type FollowersRequest = RequestOf; @@ -141,7 +141,7 @@ export const FollowingQuery = graphql( } } }`, - [Following, PaginatedResultInfo], + [Following, PaginatedResultInfoFragment], ); export type FollowingRequest = RequestOf; @@ -157,7 +157,7 @@ export const FollowersYouKnowQuery = graphql( } } }`, - [Follower, PaginatedResultInfo], + [Follower, PaginatedResultInfoFragment], ); export type FollowersYouKnowRequest = RequestOf; diff --git a/packages/graphql/src/fragments/account.ts b/packages/graphql/src/fragments/account.ts index 85ba76490..271e9315d 100644 --- a/packages/graphql/src/fragments/account.ts +++ b/packages/graphql/src/fragments/account.ts @@ -28,7 +28,7 @@ export const AccountMetadata = graphql( ); export type AccountMetadata = FragmentOf; -export const Account = graphql( +export const AccountFragment = graphql( `fragment Account on Account { __typename address @@ -38,9 +38,9 @@ export const Account = graphql( }`, [Username], ); -export type Account = FragmentOf; +export type Account = FragmentOf; -export const FullAccount = graphql( +export const FullAccountFragment = graphql( `fragment Account on Account { __typename address @@ -57,7 +57,7 @@ export const FullAccount = graphql( }`, [AccountMetadata, LoggedInAccountOperations, Username], ); -export type FullAccount = FragmentOf; +export type FullAccount = FragmentOf; const AccountManagerPermissions = graphql( `fragment AccountManagerPermissions on AccountManagerPermissions { @@ -94,7 +94,7 @@ const AccountManaged = graphql( ...AccountManagerPermissions } }`, - [AccountManagerPermissions, Account], + [AccountManagerPermissions, AccountFragment], ); export type AccountManaged = FragmentOf; @@ -112,7 +112,7 @@ export const AccountAvailable = graphql( } } }`, - [Account, AccountManaged], + [AccountFragment, AccountManaged], ); export type AccountAvailable = FragmentOf; @@ -124,6 +124,6 @@ export const AccountBlocked = graphql( ...Account } }`, - [Account], + [AccountFragment], ); export type AccountBlocked = FragmentOf; diff --git a/packages/graphql/src/fragments/pagination.ts b/packages/graphql/src/fragments/pagination.ts index 88cda01a6..c5487c0cf 100644 --- a/packages/graphql/src/fragments/pagination.ts +++ b/packages/graphql/src/fragments/pagination.ts @@ -1,11 +1,11 @@ import type { FragmentOf } from 'gql.tada'; import { graphql } from '../graphql'; -export const PaginatedResultInfo = graphql( +export const PaginatedResultInfoFragment = graphql( `fragment PaginatedResultInfo on PaginatedResultInfo { __typename prev next }`, ); -export type PaginatedResultInfo = FragmentOf; +export type PaginatedResultInfo = FragmentOf; diff --git a/packages/graphql/src/fragments/post.ts b/packages/graphql/src/fragments/post.ts index f01e80351..ea918f90e 100644 --- a/packages/graphql/src/fragments/post.ts +++ b/packages/graphql/src/fragments/post.ts @@ -1,6 +1,6 @@ import type { FragmentOf } from 'gql.tada'; import { graphql } from '../graphql'; -import { Account } from './account'; +import { AccountFragment } from './account'; import { ActionInputInfo, Amount, BooleanValue, NetworkAddress } from './common'; import { App, Feed } from './primitives'; @@ -132,9 +132,8 @@ export const ReferencedPost = graphql( operations { ...LoggedInPostOperations } - } - `, - [Account, App, Feed, PostMetadata, PostAction, LoggedInPostOperations], + }`, + [App, Feed, PostMetadata, PostAction, LoggedInPostOperations], ); export const NestedPost = graphql( @@ -150,7 +149,7 @@ export const NestedPost = graphql( ); export type NestedPost = FragmentOf; -export const Post = graphql( +export const PostFragment = graphql( `fragment Post on Post { __typename id @@ -184,9 +183,10 @@ export const Post = graphql( } } `, - [Account, App, Feed, PostMetadata, PostAction, NestedPost, LoggedInPostOperations], + [App, Feed, PostMetadata, PostAction, NestedPost, LoggedInPostOperations], ); -export type Post = FragmentOf; + +export type Post = FragmentOf; // operations: LoggedInPostOperations export const Repost = graphql( @@ -208,7 +208,7 @@ export const AnyPost = graphql( ...Repost } }`, - [Post, Repost], + [PostFragment, Repost], ); export type AnyPost = FragmentOf; @@ -277,6 +277,6 @@ export const AccountPostReaction = graphql( ...PostReaction } }`, - [Account, PostReaction], + [AccountFragment, PostReaction], ); export type AccountPostReaction = FragmentOf; diff --git a/packages/graphql/src/graphql.ts b/packages/graphql/src/graphql.ts index f6fbd2c87..36e1a0afe 100644 --- a/packages/graphql/src/graphql.ts +++ b/packages/graphql/src/graphql.ts @@ -1,29 +1,34 @@ -import { - type AccessToken, - type BigDecimal, - type BigIntString, - type BlockchainData, - type CompactJwt, - type Cursor, - type DateTime, - type EncodedTransaction, - type EvmAddress, - type ID, - type IdToken, - type LegacyProfileId, - type PostId, - type RefreshToken, - type Signature, - type TxHash, - type URI, - type URL, - type UUID, - type UsernameValue, - type Void, - never, +import type { + AccessToken, + BigDecimal, + BigIntString, + BlockchainData, + CompactJwt, + Cursor, + DateTime, + EncodedTransaction, + EvmAddress, + ID, + IdToken, + LegacyProfileId, + PostId, + RefreshToken, + Signature, + TxHash, + URI, + URL, + UUID, + UsernameValue, + Void, } from '@lens-protocol/types'; -import { type DocumentDecoration, type FragmentOf, initGraphQLTada } from 'gql.tada'; +import { + type DocumentDecoration, + type FragmentOf, + type TadaDocumentNode, + initGraphQLTada, +} from 'gql.tada'; import type { AccountReportReason, PageSize } from './enums'; +import type { PaginatedResultInfo } from './fragments'; import type { introspection } from './graphql-env'; export const graphql = initGraphQLTada<{ @@ -75,21 +80,86 @@ export type RequestOf = Document extends DocumentDecoration< */ export type FragmentShape = NonNullable[1]>[number]; -export type TypedDocumentFrom = ReturnType< - typeof graphql +type GetDocumentNode< + In extends string = string, + Fragments extends FragmentShape[] = FragmentShape[], +> = ReturnType>; + +export type AnyGqlNode = { __typename: TTypename }; + +export type AnyVariables = Record; + +export type FragmentDocumentFor = TGqlNode extends AnyGqlNode< + infer TTypename +> + ? TadaDocumentNode< + TGqlNode, + AnyVariables, + { + fragment: TTypename; + on: TTypename; + masked: false; + } + > + : never; + +/** + * A standardized data object. + * + * All GQL operations should alias their results to `value` to ensure interoperability + * with this client interface. + */ +export type StandardData = { value: T }; + +export type RequestFrom = RequestOf>; + +// biome-ignore lint/suspicious/noExplicitAny: simplifies necessary type assertions +export type StandardDocumentNode = TadaDocumentNode< + StandardData, + { request: Request } >; -export type FragmentNodeFor = T extends FragmentOf ? U : never; +type FragmentDocumentFrom< + In extends string, + Fragments extends FragmentShape[], + Document extends GetDocumentNode = GetDocumentNode, +> = Document extends FragmentShape ? Document : never; -export type Factory = []>( - ...fragments: T -) => TypedDocumentFrom; +type FragmentDocumentForEach = { + [K in keyof Nodes]: FragmentDocumentFor; +}; /** * @internal */ -export function factory(_: In): Factory { - return []>(..._fragments: T): TypedDocumentFrom => { - never('This function should never be called'); - }; +export type DynamicFragmentDocument< + In extends string, + StaticNodes extends AnyGqlNode[], +> = FragmentDocumentFrom> & { + __phantom: In; +}; + +/** + * @internal + */ +export function fragment( + input: In, + staticFragments: FragmentDocumentForEach = [] as FragmentDocumentForEach, +): DynamicFragmentDocument { + return graphql(input, staticFragments) as DynamicFragmentDocument; } + +/** + * @internal + */ +export type DynamicFragmentOf< + Document, + DynamicNodes extends AnyGqlNode[], +> = Document extends DynamicFragmentDocument + ? FragmentOf>> + : never; + +export type Paginated = { + items: readonly T[]; + pageInfo: PaginatedResultInfo; +}; diff --git a/packages/graphql/src/notifications.ts b/packages/graphql/src/notifications.ts index 75a81d5c0..ee9f75664 100644 --- a/packages/graphql/src/notifications.ts +++ b/packages/graphql/src/notifications.ts @@ -1,8 +1,17 @@ +import type { Prettify } from '@lens-protocol/types'; import type { FragmentOf } from 'gql.tada'; -import { Account, PaginatedResultInfo, Post } from './fragments'; -import { type RequestOf, graphql } from './graphql'; +import { type Account, PaginatedResultInfoFragment, PostFragment } from './fragments'; +import { + type DynamicFragmentOf, + type FragmentDocumentFor, + type Paginated, + type RequestFrom, + type StandardDocumentNode, + fragment, + graphql, +} from './graphql'; -const FollowNotification = graphql( +const FollowNotificationFragment = fragment( `fragment FollowNotification on FollowNotification { __typename id @@ -13,11 +22,13 @@ const FollowNotification = graphql( followedAt } }`, - [Account], ); -export type FollowNotification = FragmentOf; +export type FollowNotification = DynamicFragmentOf< + typeof FollowNotificationFragment, + [TAccount] +>; -const ReactionNotification = graphql( +const ReactionNotificationFragment = fragment( `fragment ReactionNotification on ReactionNotification { __typename id @@ -34,11 +45,14 @@ const ReactionNotification = graphql( ...Post } }`, - [Account, Post], + [PostFragment], ); -export type ReactionNotification = FragmentOf; +export type ReactionNotification = DynamicFragmentOf< + typeof ReactionNotificationFragment, + [TAccount] +>; -const CommentNotification = graphql( +const CommentNotificationFragment = graphql( `fragment CommentNotification on CommentNotification { __typename id @@ -46,11 +60,11 @@ const CommentNotification = graphql( ...Post } }`, - [Post], + [PostFragment], ); -export type CommentNotification = FragmentOf; +export type CommentNotification = FragmentOf; -const RepostNotification = graphql( +const RepostNotificationFragment = fragment( `fragment RepostNotification on RepostNotification { __typename id @@ -65,11 +79,13 @@ const RepostNotification = graphql( ...Post } }`, - [Account], + [PostFragment], ); -export type RepostNotification = FragmentOf; +export type RepostNotification = Prettify< + DynamicFragmentOf +>; -const QuoteNotification = graphql( +const QuoteNotificationFragment = graphql( `fragment QuoteNotification on QuoteNotification { __typename id @@ -77,11 +93,11 @@ const QuoteNotification = graphql( ...Post } }`, - [Post], + [PostFragment], ); -export type QuoteNotification = FragmentOf; +export type QuoteNotification = FragmentOf; -const MentionNotification = graphql( +const MentionNotificationFragment = graphql( `fragment MentionNotification on MentionNotification { __typename id @@ -89,55 +105,62 @@ const MentionNotification = graphql( ...Post } }`, - [Post], + [PostFragment], ); -export type MentionNotification = FragmentOf; +export type MentionNotification = FragmentOf; -const Notification = graphql( - `fragment Notification on Notification { - __typename - ... on FollowNotification { - ...FollowNotification - } - ... on ReactionNotification { - ...ReactionNotification - } - ... on CommentNotification { - ...CommentNotification - } - ... on RepostNotification { - ...RepostNotification - } - ... on QuoteNotification { - ...QuoteNotification - } - ... on MentionNotification { - ...MentionNotification - } - }`, - [ - FollowNotification, - ReactionNotification, - CommentNotification, - RepostNotification, - QuoteNotification, - MentionNotification, - ], -); -export type Notification = FragmentOf; +export type Notification = + | FollowNotification + | ReactionNotification + | CommentNotification + | RepostNotification + | QuoteNotification + | MentionNotification; -export const NotificationsQuery = graphql( - `query Notifications($request: NotificationRequest!) { +const query = ` + query Notifications($request: NotificationRequest!) { value: notifications(request: $request) { __typename items { - ...Notification + ... on FollowNotification { + ...FollowNotification + } + ... on ReactionNotification { + ...ReactionNotification + } + ... on CommentNotification { + ...CommentNotification + } + ... on RepostNotification { + ...RepostNotification + } + ... on QuoteNotification { + ...QuoteNotification + } + ... on MentionNotification { + ...MentionNotification + } } pageInfo { ...PaginatedResultInfo } } - }`, - [Notification, PaginatedResultInfo], -); -export type NotificationsRequest = RequestOf; + } +`; +export type NotificationsRequest = RequestFrom; + +export function notificationsQuery( + AccountFragment: FragmentDocumentFor, +): StandardDocumentNode>, NotificationsRequest> { + return graphql(query, [ + FollowNotificationFragment, + ReactionNotificationFragment, + CommentNotificationFragment, + RepostNotificationFragment, + QuoteNotificationFragment, + MentionNotificationFragment, + AccountFragment, + PostFragment, + PaginatedResultInfoFragment, + ]) as StandardDocumentNode; +} diff --git a/packages/graphql/src/post.ts b/packages/graphql/src/post.ts index 4fa8e3fb3..ffa123383 100644 --- a/packages/graphql/src/post.ts +++ b/packages/graphql/src/post.ts @@ -3,7 +3,7 @@ import { AccountPostReaction, ActionInfo, AnyPost, - PaginatedResultInfo, + PaginatedResultInfoFragment, SelfFundedTransactionRequest, SponsoredTransactionRequest, TransactionWillFail, @@ -88,7 +88,7 @@ export const PostActionsQuery = graphql( } } }`, - [ActionInfo, PaginatedResultInfo], + [ActionInfo, PaginatedResultInfoFragment], ); export type PostActionsRequest = RequestOf; @@ -103,7 +103,7 @@ export const PostReactionsQuery = graphql( } } }`, - [AccountPostReaction, PaginatedResultInfo], + [AccountPostReaction, PaginatedResultInfoFragment], ); export type PostReactionsRequest = RequestOf; @@ -118,7 +118,7 @@ export const PostBookmarksQuery = graphql( } } }`, - [AnyPost, PaginatedResultInfo], + [AnyPost, PaginatedResultInfoFragment], ); export type PostBookmarksRequest = RequestOf; @@ -133,7 +133,7 @@ export const PostReferencesQuery = graphql( } } }`, - [AnyPost, PaginatedResultInfo], + [AnyPost, PaginatedResultInfoFragment], ); export type PostReferencesRequest = RequestOf; diff --git a/packages/graphql/src/timeline.ts b/packages/graphql/src/timeline.ts index 9ab08dd74..b18c19691 100644 --- a/packages/graphql/src/timeline.ts +++ b/packages/graphql/src/timeline.ts @@ -1,5 +1,5 @@ import type { FragmentOf } from 'gql.tada'; -import { PaginatedResultInfo, Post } from './fragments'; +import { PaginatedResultInfoFragment, PostFragment } from './fragments'; import { type RequestOf, graphql } from './graphql'; const TimelineItem = graphql( @@ -16,7 +16,7 @@ const TimelineItem = graphql( ...Post } }`, - [Post], + [PostFragment], ); export type TimelineItem = FragmentOf; @@ -32,7 +32,7 @@ export const TimelineQuery = graphql( } } }`, - [TimelineItem, PaginatedResultInfo], + [TimelineItem, PaginatedResultInfoFragment], ); export type TimelineRequest = RequestOf; @@ -48,6 +48,6 @@ export const TimelineHighlightsQuery = graphql( } } }`, - [Post, PaginatedResultInfo], + [PostFragment, PaginatedResultInfoFragment], ); export type TimelineHighlightsRequest = RequestOf; diff --git a/packages/react/src/provider.tsx b/packages/react/src/provider.tsx index e9cd76eb4..17d438a64 100644 --- a/packages/react/src/provider.tsx +++ b/packages/react/src/provider.tsx @@ -1,13 +1,14 @@ import type { PublicClient } from '@lens-protocol/client'; import React from 'react'; +import type { Context } from '@lens-protocol/client'; import type { ReactNode } from 'react'; import { LensContextProvider } from './context'; /** * props */ -export type ProviderProps = { +export type ProviderProps = { /** * The children to render */ @@ -15,7 +16,7 @@ export type ProviderProps = { /** * The configuration for the Lens SDK */ - client: PublicClient; + client: PublicClient; }; /** @@ -36,6 +37,6 @@ export type ProviderProps = { * } * ``` */ -export function Provider({ children, client }: ProviderProps) { +export function Provider({ children, client }: ProviderProps) { return {children}; } diff --git a/packages/types/package.json b/packages/types/package.json index 803b0e6bc..81ec394d4 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -18,11 +18,11 @@ }, "license": "MIT", "dependencies": { - "neverthrow": "^8.0.0" + "neverthrow": "^8.0.0", + "type-fest": "^4.26.1" }, "devDependencies": { "tsup": "^8.3.5", - "type-fest": "^4.26.1", "typescript": "^5.6.3" } } diff --git a/packages/types/src/helpers/assertions.ts b/packages/types/src/helpers/assertions.ts index adc83785b..e3cc5b63c 100644 --- a/packages/types/src/helpers/assertions.ts +++ b/packages/types/src/helpers/assertions.ts @@ -1,6 +1,6 @@ import type { Err, Ok, Result } from 'neverthrow'; import type { UnknownRecord } from 'type-fest'; -import { InvariantError, invariant } from './invariant'; +import { InvariantError } from './invariant'; function isObject(value: unknown): value is UnknownRecord { const type = typeof value; @@ -27,12 +27,16 @@ export function assertNever(x: never, message = `Unexpected object: ${String(x)} * Asserts that the given `Result` is an `Ok` variant. */ export function assertOk(result: Result): asserts result is Ok { - invariant(result.isOk(), 'Expected result to be Ok'); + if (result.isErr()) { + throw new InvariantError(`Expected result to be Ok: ${result.error.message}`); + } } /** * Asserts that the given `Result` is an `Err` variant. */ export function assertErr(result: Result): asserts result is Err { - invariant(result.isErr(), 'Expected result to be Err'); + if (result.isOk()) { + throw new InvariantError(`Expected result to be Err: ${result.value}`); + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de4a3057f..154fd81b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,21 +184,21 @@ importers: neverthrow: specifier: ^8.0.0 version: 8.0.0 + type-fest: + specifier: ^4.26.1 + version: 4.26.1 devDependencies: tsup: specifier: ^8.3.5 version: 8.3.5(postcss@8.4.49)(tsx@4.19.2)(typescript@5.6.3) - type-fest: - specifier: ^4.26.1 - version: 4.26.1 typescript: specifier: ^5.6.3 version: 5.6.3 packages: - '@0no-co/graphql.web@1.0.9': - resolution: {integrity: sha512-lXSg4bDHvP8CiMdpQf9f/rca12IIjXHN/p0Rc5mgzgLe4JBlIoA1zFa9NKhfG1bW0OyI2hgaOldFCfkEQwZuEQ==} + '@0no-co/graphql.web@1.0.12': + resolution: {integrity: sha512-BTDjjsV/zSPy5fqItwm+KWUfh9CSe9tTtR6rCB72ddtkAxdcHbi4Ir4r/L1Et4lyxmL+i7Rb3m9sjLLi9tYrzA==} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 peerDependenciesMeta: @@ -2670,7 +2670,7 @@ packages: snapshots: - '@0no-co/graphql.web@1.0.9(graphql@16.9.0)': + '@0no-co/graphql.web@1.0.12(graphql@16.9.0)': optionalDependencies: graphql: 16.9.0 @@ -3099,7 +3099,7 @@ snapshots: '@gql.tada/internal@1.0.8(graphql@16.9.0)(typescript@5.6.3)': dependencies: - '@0no-co/graphql.web': 1.0.9(graphql@16.9.0) + '@0no-co/graphql.web': 1.0.12(graphql@16.9.0) graphql: 16.9.0 typescript: 5.6.3 @@ -3354,7 +3354,7 @@ snapshots: '@urql/core@5.0.8(graphql@16.9.0)': dependencies: - '@0no-co/graphql.web': 1.0.9(graphql@16.9.0) + '@0no-co/graphql.web': 1.0.12(graphql@16.9.0) wonka: 6.3.4 transitivePeerDependencies: - graphql @@ -3915,7 +3915,7 @@ snapshots: gql.tada@1.8.10(graphql@16.9.0)(typescript@5.6.3): dependencies: - '@0no-co/graphql.web': 1.0.9(graphql@16.9.0) + '@0no-co/graphql.web': 1.0.12(graphql@16.9.0) '@0no-co/graphqlsp': 1.12.16(graphql@16.9.0)(typescript@5.6.3) '@gql.tada/cli-utils': 1.6.3(graphql@16.9.0)(typescript@5.6.3) '@gql.tada/internal': 1.0.8(graphql@16.9.0)(typescript@5.6.3)