diff --git a/examples/custom-fragments/README.md b/examples/custom-fragments/README.md new file mode 100644 index 000000000..78a035b54 --- /dev/null +++ b/examples/custom-fragments/README.md @@ -0,0 +1,3 @@ +# Custom Fragments + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/lens-protocol/lens-sdk/tree/next/examples/custom-fragments) diff --git a/examples/custom-fragments/index.html b/examples/custom-fragments/index.html new file mode 100644 index 000000000..b4529522a --- /dev/null +++ b/examples/custom-fragments/index.html @@ -0,0 +1,18 @@ + + + + + + + + +

Custom Fragments

+
Loading...
+ + + diff --git a/examples/custom-fragments/index.ts b/examples/custom-fragments/index.ts new file mode 100644 index 000000000..5d516eb87 --- /dev/null +++ b/examples/custom-fragments/index.ts @@ -0,0 +1,41 @@ +import 'viem/window'; + +import { + type FragmentOf, + PublicClient, + evmAddress, + graphql, + testnet as protocolTestnet, +} from '@lens-protocol/client'; +import { fetchAccount } from '@lens-protocol/client/actions'; + +const MyAccountFragment = graphql( + `fragment Account on Account { + __typename + address + username { + value + } + metadata { + __typename + name + picture + } + }`, +); + +type MyAccount = FragmentOf; + +const client = PublicClient.create({ + environment: protocolTestnet, + accountFragment: MyAccountFragment, +}); + +const account: MyAccount | null = await fetchAccount(client, { + address: evmAddress('0x57b62a1571F4F09CDB4C3d93dA542bfe142D9F81'), +}).unwrapOr(null); + +export default [ + `

${account?.username?.value}

`, + `
${JSON.stringify(account, null, 2)}
`, +]; diff --git a/examples/custom-fragments/package.json b/examples/custom-fragments/package.json new file mode 100644 index 000000000..dea64d2e6 --- /dev/null +++ b/examples/custom-fragments/package.json @@ -0,0 +1,19 @@ +{ + "name": "example-custom-fragments", + "private": true, + "type": "module", + "scripts": { + "dev": "vite" + }, + "dependencies": { + "@lens-network/sdk": "canary", + "@lens-protocol/client": "file:../../packages/client", + "@lens-protocol/metadata": "next", + "@lens-protocol/storage-node-client": "next", + "viem": "^2.21.55" + }, + "devDependencies": { + "typescript": "^5.6.3", + "vite": "^5.4.11" + } +} diff --git a/examples/custom-fragments/tsconfig.json b/examples/custom-fragments/tsconfig.json new file mode 100644 index 000000000..6da89b87d --- /dev/null +++ b/examples/custom-fragments/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["ESNext", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "include": ["./"] +} diff --git a/packages/client/src/actions/account.ts b/packages/client/src/actions/account.ts index 4606b3204..3a2b9650b 100644 --- a/packages/client/src/actions/account.ts +++ b/packages/client/src/actions/account.ts @@ -33,12 +33,10 @@ import type { import { AccountFeedsStatsQuery, AccountGraphsStatsQuery, - AccountQuery, AccountStatsQuery, AccountsAvailableQuery, AccountsBlockedQuery, AccountsBulkQuery, - AccountsQuery, BlockMutation, CreateAccountWithUsernameMutation, EnableSignlessMutation, @@ -50,10 +48,13 @@ import { UnblockMutation, UndoRecommendAccountMutation, UnmuteAccountMutation, + accountQuery, + accountsQuery, } 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'; /** @@ -71,11 +72,11 @@ import type { UnauthenticatedError, UnexpectedError } from '../errors'; * @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 { + return client.query(accountQuery([client.context.accountFragment]), { request }); } /** @@ -84,18 +85,24 @@ export function fetchAccount( * Using a {@link SessionClient} will yield {@link Account#operations} specific to the authenticated Account. * * ```ts - * const result = await fetchAccounts(anyClient); + * const result = await fetchAccounts(anyClient, { + * filter: { + * searchBy: { + * localNameQuery: 'stani', + * } + * } + * }); * ``` * * @param client - Any Lens client. * @param request - The query request. * @returns The list of accounts. */ -export function fetchAccounts( - client: AnyClient, +export function fetchAccounts( + client: AnyClient>, request: AccountsRequest = {}, -): ResultAsync | null, UnexpectedError> { - return client.query(AccountsQuery, { request }); +): ResultAsync | null, UnexpectedError> { + return client.query(accountsQuery([client.context.accountFragment]), { request }); } /** diff --git a/packages/client/src/actions/authentication.ts b/packages/client/src/actions/authentication.ts index 9c6ea179c..7cfba3d08 100644 --- a/packages/client/src/actions/authentication.ts +++ b/packages/client/src/actions/authentication.ts @@ -14,14 +14,16 @@ import { AuthenticatedSessionsQuery, CurrentSessionQuery, LegacyRolloverRefreshMutation, - MeQuery, RefreshMutation, RevokeAuthenticationMutation, SwitchAccountMutation, + meQuery, } from '@lens-protocol/graphql'; import type { ResultAsync } from '@lens-protocol/types'; +import type { Account } from '@lens-protocol/graphql'; import type { AnyClient, SessionClient } from '../clients'; +import type { Context } from '../context'; import type { UnauthenticatedError, UnexpectedError } from '../errors'; /** @@ -151,8 +153,8 @@ export function switchAccount( * @param client - The session client for the authenticated Account. * @returns The details of the authenticated Account. */ -export function fetchMeDetails( - client: SessionClient, -): ResultAsync { - return client.query(MeQuery, {}); +export function fetchMeDetails( + client: SessionClient>, +): ResultAsync, UnauthenticatedError | UnexpectedError> { + return client.query(meQuery([client.context.accountFragment]), {}); } diff --git a/packages/client/src/actions/posts.ts b/packages/client/src/actions/posts.ts index 7f854fb27..2ab301689 100644 --- a/packages/client/src/actions/posts.ts +++ b/packages/client/src/actions/posts.ts @@ -1,4 +1,5 @@ import type { + Account, AccountPostReaction, ActionInfo, AnyPost, @@ -6,35 +7,36 @@ import type { Post, PostActionsRequest, PostBookmarksRequest, + PostEdit, + PostEditsRequest, + PostFields, + PostReactionStatus, + PostReactionStatusRequest, PostReactionsRequest, PostReferencesRequest, PostRequest, + PostTagsRequest, PostsRequest, + WhoActedOnPostQueryRequest, + WhoReferencedPostRequest, } from '@lens-protocol/graphql'; import { PostActionsQuery, - PostBookmarksQuery, PostEditsQuery, - PostQuery, PostReactionStatusQuery, PostReactionsQuery, - PostReferencesQuery, PostTagsQuery, - PostsQuery, WhoActedOnPostQuery, WhoReferencedPostQuery, + postBookmarksQuery, + postQuery, + postReferencesQuery, + postsQuery, } from '@lens-protocol/graphql'; import type { ResultAsync } from '@lens-protocol/types'; -import type { PostTagsRequest } from '@lens-protocol/graphql'; -import type { PostReactionStatusRequest } from '@lens-protocol/graphql'; -import type { PostReactionStatus } from '@lens-protocol/graphql'; -import type { WhoReferencedPostRequest } from '@lens-protocol/graphql'; -import type { Account } from '@lens-protocol/graphql'; -import type { WhoActedOnPostQueryRequest } from '@lens-protocol/graphql'; -import type { PostEditsRequest } from '@lens-protocol/graphql'; -import type { PostEdit } from '@lens-protocol/graphql'; import type { AnyClient, SessionClient } from '../clients'; +import type { Context } from '../context'; import type { UnauthenticatedError, UnexpectedError } from '../errors'; /** @@ -53,11 +55,14 @@ import type { UnauthenticatedError, UnexpectedError } from '../errors'; * @param request - The query request. * @returns The Post or `null` if it does not exist. */ -export function fetchPost( - client: AnyClient, +export function fetchPost( + client: AnyClient>, request: PostRequest, -): ResultAsync { - return client.query(PostQuery, { request }); +): ResultAsync | null, UnexpectedError> { + return client.query( + postQuery([client.context.postFieldsFragment, client.context.accountFragment]), + { request }, + ); } /** @@ -82,7 +87,7 @@ export function fetchPosts( client: AnyClient, request: PostsRequest, ): ResultAsync, UnexpectedError> { - return client.query(PostsQuery, { request }); + return client.query(postsQuery, { request }); } /** @@ -138,7 +143,7 @@ export function fetchPostBookmarks( client: SessionClient, request: PostBookmarksRequest = {}, ): ResultAsync, UnexpectedError | UnauthenticatedError> { - return client.query(PostBookmarksQuery, { request }); + return client.query(postBookmarksQuery, { request }); } /** @@ -158,7 +163,7 @@ export function fetchPostReferences( client: AnyClient, request: PostReferencesRequest, ): ResultAsync, UnexpectedError | UnauthenticatedError> { - return client.query(PostReferencesQuery, { request }); + return client.query(postReferencesQuery, { request }); } /** diff --git a/packages/client/src/actions/timeline.ts b/packages/client/src/actions/timeline.ts index b6fa7b130..616202bab 100644 --- a/packages/client/src/actions/timeline.ts +++ b/packages/client/src/actions/timeline.ts @@ -5,7 +5,7 @@ import type { TimelineItem, TimelineRequest, } from '@lens-protocol/graphql'; -import { TimelineHighlightsQuery, TimelineQuery } from '@lens-protocol/graphql'; +import { timelineHighlightsQuery, timelineQuery } from '@lens-protocol/graphql'; import type { ResultAsync } from '@lens-protocol/types'; import type { AnyClient } from '../clients'; @@ -28,7 +28,7 @@ export function fetchTimeline( client: AnyClient, request: TimelineRequest, ): ResultAsync | null, UnexpectedError> { - return client.query(TimelineQuery, { request }); + return client.query(timelineQuery, { request }); } /** @@ -48,5 +48,5 @@ export function fetchTimelineHighlights( client: AnyClient, request: TimelineHighlightsRequest, ): ResultAsync, UnexpectedError> { - return client.query(TimelineHighlightsQuery, { request }); + return client.query(timelineHighlightsQuery, { request }); } diff --git a/packages/client/src/clients.ts b/packages/client/src/clients.ts index 35f592578..669f5a440 100644 --- a/packages/client/src/clients.ts +++ b/packages/client/src/clients.ts @@ -32,7 +32,7 @@ import { type AuthConfig, authExchange } from '@urql/exchange-auth'; import { type AuthenticatedUser, authenticatedUser } from './AuthenticatedUser'; import { switchAccount, transactionStatus } from './actions'; import type { ClientConfig } from './config'; -import { type Context, configureContext } from './context'; +import { type Context, type ContextFrom, configureContext } from './context'; import { AuthenticationError, GraphQLErrorCode, @@ -151,7 +151,9 @@ export class PublicClient extends AbstractCl * @param options - The options to configure the client. * @returns The new instance of the client. */ - static create(options: ClientConfig): PublicClient { + static create( + options: TConfig, + ): PublicClient> { return new PublicClient(configureContext(options)); } diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts index 2e743c545..2dadb38cc 100644 --- a/packages/client/src/config.ts +++ b/packages/client/src/config.ts @@ -1,10 +1,15 @@ import type { EnvironmentConfig } from '@lens-protocol/env'; +import type { Account, PostFields } from '@lens-protocol/graphql'; +import type { FragmentDocumentFor } from '@lens-protocol/graphql'; import type { IStorageProvider } from '@lens-protocol/storage'; /** * The client configuration. */ -export type ClientConfig = { +export type ClientConfig< + TAccount extends Account = Account, + TPostFields extends PostFields = PostFields, +> = { /** * The environment configuration to use (e.g. `mainnet`, `testnet`). */ @@ -27,11 +32,22 @@ export type ClientConfig = { * Use this to set the `Origin` header for requests from non-browser environments. */ origin?: string; - /** * The storage provider to use. * * @defaultValue {@link InMemoryStorageProvider} */ storage?: IStorageProvider; + /** + * The Account Fragment to use. + * + * @defaultValue {@link AccountFragment} + */ + accountFragment?: FragmentDocumentFor; + /** + * The Post Fragment to use. + * + * @defaultValue {@link PostFragment} + */ + postFieldsFragment?: FragmentDocumentFor; }; diff --git a/packages/client/src/context.ts b/packages/client/src/context.ts index 995d799f3..538faafc0 100644 --- a/packages/client/src/context.ts +++ b/packages/client/src/context.ts @@ -1,27 +1,45 @@ import type { EnvironmentConfig } from '@lens-protocol/env'; +import { type Account, AccountFragment, PostFieldsFragment } from '@lens-protocol/graphql'; +import type { FragmentDocumentFor, PostFields } 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, + TPostFields extends PostFields = PostFields, +> = { environment: EnvironmentConfig; cache: boolean; debug: boolean; origin?: string; storage: IStorageProvider; + accountFragment: FragmentDocumentFor; + postFieldsFragment: FragmentDocumentFor; }; /** * @internal */ -export function configureContext(from: ClientConfig): Context { +export type ContextFrom = TConfig extends ClientConfig + ? Context + : never; + +/** + * @internal + */ +export function configureContext( + from: TConfig, +): ContextFrom { return { environment: from.environment, cache: from.cache ?? false, debug: from.debug ?? false, origin: from.origin, storage: from.storage ?? new InMemoryStorageProvider(), - }; + accountFragment: from.accountFragment ?? AccountFragment, + postFieldsFragment: from.postFieldsFragment ?? PostFieldsFragment, + } as ContextFrom; } diff --git a/packages/client/testing-utils.ts b/packages/client/testing-utils.ts index 0f7bfe8a2..f44d9ed18 100644 --- a/packages/client/testing-utils.ts +++ b/packages/client/testing-utils.ts @@ -4,6 +4,7 @@ import { evmAddress } from '@lens-protocol/types'; import { http, type Account, type Transport, type WalletClient, createWalletClient } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; +import { FullAccountFragment, FullPostFieldsFragment } from '@lens-protocol/graphql'; import { GraphQLErrorCode, PublicClient, testnet as apiEnv } from './src'; const pk = privateKeyToAccount(import.meta.env.PRIVATE_KEY); @@ -16,6 +17,8 @@ export function createPublicClient() { return PublicClient.create({ environment: apiEnv, origin: 'http://example.com', + accountFragment: FullAccountFragment, + postFieldsFragment: FullPostFieldsFragment, }); } diff --git a/packages/graphql/src/accounts/account.ts b/packages/graphql/src/accounts/account.ts index 4be974022..7caa73e58 100644 --- a/packages/graphql/src/accounts/account.ts +++ b/packages/graphql/src/accounts/account.ts @@ -8,23 +8,23 @@ import { SponsoredTransactionRequestFragment, TransactionWillFailFragment, } from '../fragments'; -import { type RequestOf, graphql } from '../graphql'; +import { type RequestOf, dynamic, graphql } from '../graphql'; -export const AccountQuery = graphql( +export const accountQuery = dynamic( `query Account($request: AccountRequest!) { value: account(request: $request) { ...Account } }`, - [AccountFragment], + [], ); -export type AccountRequest = RequestOf; +export type AccountRequest = RequestOf; -export const AccountsQuery = graphql( +export const accountsQuery = dynamic( `query Accounts($request: AccountsRequest!) { value: accounts(request: $request) { __typename - items{ + items { ...Account } pageInfo { @@ -32,9 +32,9 @@ export const AccountsQuery = graphql( } } }`, - [AccountFragment, PaginatedResultInfoFragment], + [PaginatedResultInfoFragment], ); -export type AccountsRequest = RequestOf; +export type AccountsRequest = RequestOf; export const AccountsBulkQuery = graphql( `query AccountsBulk($request: AccountsBulkRequest!) { @@ -222,7 +222,7 @@ export const AccountGraphsStatsQuery = graphql( export type AccountGraphsStatsRequest = RequestOf; -export const AccountsAvailableQuery = graphql( +export const AccountsAvailableQuery = dynamic( `query AccountsAvailable($request: AccountsAvailableRequest!) { value: accountsAvailable(request: $request) { items{ diff --git a/packages/graphql/src/authentication.ts b/packages/graphql/src/authentication.ts index 32a3ceb8b..6dc6e2f08 100644 --- a/packages/graphql/src/authentication.ts +++ b/packages/graphql/src/authentication.ts @@ -1,6 +1,18 @@ import type { FragmentOf } from 'gql.tada'; -import { AccountAvailableFragment, PaginatedResultInfoFragment } from './fragments'; -import { type RequestOf, graphql } from './graphql'; +import { + type Account, + type AccountAvailable, + AccountAvailableFragment, + PaginatedResultInfoFragment, +} from './fragments'; +import { + type FragmentDocumentFor, + type PartialFragmentOf, + type RequestOf, + dynamic, + graphql, + partial, +} from './graphql'; const AuthenticationChallengeFragment = graphql( `fragment AuthenticationChallenge on AuthenticationChallenge { @@ -198,7 +210,7 @@ export const SwitchAccountMutation = graphql( ); export type SwitchAccountRequest = RequestOf; -const MeResultFragment = graphql( +const MeResultFragment = partial( `fragment MeResult on MeResult { appLoggedIn isSignless @@ -210,17 +222,20 @@ const MeResultFragment = graphql( window } loggedInAs { - ...AccountAvailable + ${'...AccountAvailable'} } }`, [AccountAvailableFragment], ); -export type MeResult = FragmentOf; +export type MeResult = PartialFragmentOf< + typeof MeResultFragment, + [FragmentDocumentFor>] +>; -export const MeQuery = graphql( +export const meQuery = dynamic( `query Me { value: me { - ...MeResult + ${'...MeResult'} } }`, [MeResultFragment], diff --git a/packages/graphql/src/fragments/account.ts b/packages/graphql/src/fragments/account.ts index 42e1a10ab..2c2074962 100644 --- a/packages/graphql/src/fragments/account.ts +++ b/packages/graphql/src/fragments/account.ts @@ -1,5 +1,5 @@ import type { FragmentOf } from 'gql.tada'; -import { graphql } from '../graphql'; +import { type FragmentDocumentFor, type PartialFragmentOf, graphql, partial } from '../graphql'; import { OperationValidationOutcomeFragment } from './common'; import { MetadataAttributeFragment } from './metadata'; import { UsernameFragment } from './username'; @@ -45,6 +45,34 @@ export const AccountMetadataFragment = graphql( export type AccountMetadata = FragmentOf; export const AccountFragment = graphql( + `fragment Account on Account { + __typename + address + }`, +); +export type Account = FragmentOf; + +/** + * @deprecated Define your own AccountFragment instead using {@link graphql} and {@link FragmentOf}. + * + * @example + * ```ts + * const AccountFragment = graphql( + * `fragment Account on Account { + * __typename + * address + * owner + * username { + * ...Username + * } + * }`, + * [UsernameFragment], + * ); + * + * type Account = FragmentOf; + * ``` + */ +export const FullAccountFragment = graphql( `fragment Account on Account { __typename address @@ -63,7 +91,10 @@ export const AccountFragment = graphql( }`, [AccountMetadataFragment, LoggedInAccountOperationsFragment, UsernameFragment], ); -export type Account = FragmentOf; +/** + * @deprecated Define your own FullAccountFragment instead using {@link graphql} and {@link FragmentOf}. + */ +export type FullAccount = FragmentOf; const AccountManagerPermissionsFragment = graphql( `fragment AccountManagerPermissions on AccountManagerPermissions { @@ -89,38 +120,54 @@ export const AccountManagerFragment = graphql( ); export type AccountManager = FragmentOf; -const AccountManagedFragment = graphql( +const AccountManagedFragment = partial( `fragment AccountManaged on AccountManaged { __typename addedAt + permissions { + ...AccountManagerPermissions + } account { ...Account } - permissions { - ...AccountManagerPermissions + }`, + [AccountManagerPermissionsFragment], +); +export type AccountManaged = PartialFragmentOf< + typeof AccountManagedFragment, + [FragmentDocumentFor] +>; + +const AccountOwnedFragment = partial( + `fragment AccountOwned on AccountOwned { + __typename + addedAt + account { + ...Account } }`, - [AccountManagerPermissionsFragment, AccountFragment], ); -export type AccountManaged = FragmentOf; -export const AccountAvailableFragment = graphql( +export type AccountOwned = PartialFragmentOf< + typeof AccountOwnedFragment, + [FragmentDocumentFor] +>; + +export const AccountAvailableFragment = partial( `fragment AccountAvailable on AccountAvailable { __typename ... on AccountManaged { ...AccountManaged } ... on AccountOwned { - __typename - addedAt - account { - ...Account - } + ...AccountOwned } }`, - [AccountFragment, AccountManagedFragment], + [AccountManagedFragment, AccountOwnedFragment], ); -export type AccountAvailable = FragmentOf; +export type AccountAvailable = + | AccountManaged + | AccountOwned; export const AccountBlockedFragment = graphql( `fragment AccountBlocked on AccountBlocked { diff --git a/packages/graphql/src/fragments/post.ts b/packages/graphql/src/fragments/post.ts index ef08f23e5..1a38badac 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 { AccountFragment } from './account'; +import { type FragmentDocumentFor, type PartialFragmentOf, graphql, partial } from '../graphql'; +import { type Account, AccountFragment } from './account'; import { ActionInputInfoFragment, AmountFragment, @@ -15,6 +15,7 @@ import { EmbedMetadataFragment, EventMetadataFragment, ImageMetadataFragment, + LinkMetadataFragment, LivestreamMetadataFragment, MintMetadataFragment, SpaceMetadataFragment, @@ -56,9 +57,7 @@ export const SimpleCollectActionSettingsFragment = graphql( }`, [AmountFragment, NetworkAddressFragment, RecipientDataOutputFragment], ); -export type SimpleCollectActionSettingsFragment = FragmentOf< - typeof SimpleCollectActionSettingsFragment ->; +export type SimpleCollectActionSettings = FragmentOf; export const UnknownActionSettingsFragment = graphql( `fragment UnknownActionSettings on UnknownActionSettings { @@ -75,6 +74,7 @@ export const UnknownActionSettingsFragment = graphql( ); export type UnknownActionSettings = FragmentOf; +export type PostAction = SimpleCollectActionSettings | UnknownActionSettings; export const PostActionFragment = graphql( `fragment PostAction on PostAction { ... on SimpleCollectActionSettings { @@ -86,9 +86,11 @@ export const PostActionFragment = graphql( }`, [SimpleCollectActionSettingsFragment, UnknownActionSettingsFragment], ); -export type PostAction = FragmentOf; -export const PostMetadataFragment = graphql( +/** + * @deprecated Define your own PostMetadataFragment instead using {@link graphql} and {@link FragmentOf}. + */ +export const FullPostMetadataFragment = graphql( `fragment PostMetadata on PostMetadata { ... on ArticleMetadata { ...ArticleMetadata @@ -114,6 +116,9 @@ export const PostMetadataFragment = graphql( ... on EventMetadata { ...EventMetadata } + ... on LinkMetadata { + ...LinkMetadata + } ... on LivestreamMetadata { ...LivestreamMetadata } @@ -136,21 +141,23 @@ export const PostMetadataFragment = graphql( [ ArticleMetadataFragment, AudioMetadataFragment, - TextOnlyMetadataFragment, CheckingInMetadataFragment, - ImageMetadataFragment, - VideoMetadataFragment, EmbedMetadataFragment, EventMetadataFragment, + ImageMetadataFragment, LivestreamMetadataFragment, MintMetadataFragment, SpaceMetadataFragment, StoryMetadataFragment, + TextOnlyMetadataFragment, ThreeDMetadataFragment, + LinkMetadataFragment, TransactionMetadataFragment, + VideoMetadataFragment, ], ); -export type PostMetadata = FragmentOf; + +export type FullPostMetadata = FragmentOf; export const LoggedInPostOperationsFragment = graphql( `fragment LoggedInPostOperations on LoggedInPostOperations { @@ -190,41 +197,6 @@ export const LoggedInPostOperationsFragment = graphql( ); export type LoggedInPostOperations = FragmentOf; -export const ReferencedPostFragment = graphql( - `fragment ReferencedPost on Post { - __typename - id - author { - ...Account - } - feed { - ...Feed - } - timestamp - app { - ...App - } - metadata { - ...PostMetadata - } - actions { - ...PostAction - } - operations { - ...LoggedInPostOperations - } - } - `, - [ - AccountFragment, - AppFragment, - FeedFragment, - PostMetadataFragment, - PostActionFragment, - LoggedInPostOperationsFragment, - ], -); - export const PostStatsFragment = graphql( `fragment PostStats on PostStats { __typename @@ -238,80 +210,179 @@ export const PostStatsFragment = graphql( ); export type PostStats = FragmentOf; -export const PostFragment = graphql( - `fragment Post on Post { +export const PostFieldsFragment = graphql( + `fragment PostFields on Post { + __typename + id + timestamp + slug + }`, +); +export type PostFields = FragmentOf; + +/** + * @deprecated Define your own PostFieldsFragment instead using {@link graphql} and {@link FragmentOf}. + * + * @example + * ```ts + * const PostFieldsFragment = graphql( + * `fragment PostFields on Post { + * __typename + * id + * timestamp + * metadata { + * ...PostMetadata + * } + * }`, + * [], + * ); + * + * type PostFields = FragmentOf; + * ``` + */ +export const FullPostFieldsFragment = graphql( + `fragment PostFields on Post { __typename id - author { - ...Account - } - feed { - ...Feed - } timestamp slug - stats { - ...PostStats - } app { ...App } + feed { + ...Feed + } metadata { ...PostMetadata } - root { - ...ReferencedPost - } - quoteOf { - ...ReferencedPost - } - commentOn { - ...ReferencedPost - } - actions { - ...PostAction - } operations { ...LoggedInPostOperations } - } - `, + stats { + ...PostStats + } + }`, [ - AccountFragment, AppFragment, FeedFragment, - PostMetadataFragment, - PostActionFragment, - PostStatsFragment, - ReferencedPostFragment, LoggedInPostOperationsFragment, + FullPostMetadataFragment, + PostStatsFragment, ], ); -export type Post = FragmentOf; +/** + * @deprecated Define your own FullPostFieldsFragment instead using {@link graphql} and {@link FragmentOf}. + */ +export type FullPostFields = FragmentOf; + +export const PostFragment = partial( + `fragment Post on Post { + ${'...PostFields'} + + author { + ...Account + } + root { + ${'...PostFields'} + + author { + ...Account + } + } + quoteOf { + ${'...PostFields'} + + author { + ...Account + } + } + commentOn { + ${'...PostFields'} + + author { + ...Account + } + } + }`, +); + +export type Post< + TPostFields extends PostFields = PostFields, + TAccount extends Account = Account, +> = PartialFragmentOf< + typeof PostFragment, + [FragmentDocumentFor, FragmentDocumentFor] +>; -// operations: LoggedInPostOperations -export const RepostFragment = graphql( +export const RepostFragment = partial( `fragment Repost on Repost { __typename id + author { + ...Account + } + isDeleted + timestamp + app { + ...App + } + repostOf { + ${'...PostFields'} + + author { + ...Account + } + + root { + ${'...PostFields'} + + author { + ...Account + } + } + quoteOf { + ${'...PostFields'} + + author { + ...Account + } + } + commentOn { + ${'...PostFields'} + + author { + ...Account + } + } + } }`, - [], + [AppFragment], ); -export type Repost = FragmentOf; -export const AnyPostFragment = graphql( +export type Repost< + TPostFields extends PostFields = PostFields, + TAccount extends Account = Account, +> = PartialFragmentOf< + typeof RepostFragment, + [FragmentDocumentFor, FragmentDocumentFor] +>; + +export const AnyPostFragment = partial( `fragment AnyPost on AnyPost { ...on Post { - ...Post + ${'...Post'} } ...on Repost { - ...Repost + ${'...Repost'} } }`, [PostFragment, RepostFragment], ); -export type AnyPost = FragmentOf; +export type AnyPost< + TPostFields extends PostFields = PostFields, + TAccount extends Account = Account, +> = Post | Repost; export const KnownActionFragment = graphql( `fragment KnownAction on KnownAction { diff --git a/packages/graphql/src/graphql.ts b/packages/graphql/src/graphql.ts index 299cbdd0a..0ee9cd7fe 100644 --- a/packages/graphql/src/graphql.ts +++ b/packages/graphql/src/graphql.ts @@ -28,7 +28,6 @@ import { type TadaDocumentNode, initGraphQLTada, } from 'gql.tada'; -import type { StandardData } from './common'; import type { AccessConditionComparison, AccountReportReason, @@ -82,6 +81,9 @@ import type { } from './enums'; import type { introspection } from './graphql-env'; +/** + * A function that may be used to create documents typed using the Lens API GraphQL schema. + */ export const graphql = initGraphQLTada<{ disableMasking: true; introspection: introspection; @@ -164,15 +166,7 @@ export const graphql = initGraphQLTada<{ }; }>(); -/** - * @internal - */ -export type RequestOf = Document extends DocumentDecoration< - unknown, - { request: infer Request } -> - ? Request - : never; +export type { FragmentOf, TadaDocumentNode }; /** * @internal @@ -184,82 +178,136 @@ type GetDocumentNode< Fragments extends FragmentShape[] = FragmentShape[], > = ReturnType>; -export type AnyGqlNode = { __typename: TTypename }; - -export type AnyVariables = Record; +/** + * @internal + */ +export type AnySelectionSet = Record; /** * @internal */ -export type FragmentDocumentFor = TGqlNode extends AnyGqlNode< - infer TTypename -> - ? TadaDocumentNode< - TGqlNode, - AnyVariables, - { - fragment: TTypename; - on: TTypename; - masked: false; - } - > - : never; +export type AnyVariables = Record; -export type RequestFrom = RequestOf>; +type TypedSelectionSet = { __typename: TTypename }; -// biome-ignore lint/suspicious/noExplicitAny: simplifies necessary type assertions -export type StandardDocumentNode = TadaDocumentNode< - StandardData, - { request: Request } +/** + * @internal + */ +export type FragmentDocumentFor< + TGqlNode extends AnySelectionSet, + TTypename extends string = TGqlNode extends TypedSelectionSet + ? TTypename + : never, + TFragmentName extends string = TTypename, +> = TadaDocumentNode< + TGqlNode, + AnyVariables, + { + fragment: TFragmentName; + on: TTypename; + masked: false; + } >; +/** + * Asserts that the node is of a specific type in a union. + * + * ```ts + * type A = { __typename: 'A', a: string }; + * type B = { __typename: 'B', b: string }; + * + * const node: A | B = { __typename: 'A', a: 'a' }; + * + * assertTypename(node, 'A'); + * + * console.log(node.a); // OK + * ``` + * + * @param node - The node to assert the typename of + * @param typename - The expected typename + */ +export function assertTypename( + node: TypedSelectionSet, + typename: Typename, +): asserts node is TypedSelectionSet { + if (node.__typename !== typename) { + throw new InvariantError( + `Expected node to have typename "${typename}", but got "${node.__typename}"`, + ); + } +} + type FragmentDocumentFrom< In extends string, - Fragments extends FragmentShape[], - Document extends GetDocumentNode = GetDocumentNode, -> = Document extends FragmentShape ? Document : never; - -type FragmentDocumentForEach = { - [K in keyof Nodes]: FragmentDocumentFor; -}; + Fragments extends FragmentShape[] = FragmentShape[], +> = GetDocumentNode extends FragmentShape ? GetDocumentNode : never; /** * @internal */ -export type DynamicFragmentDocument< - In extends string, - StaticNodes extends AnyGqlNode[], -> = FragmentDocumentFrom> & { +export type PartialFragment< + In extends string = string, + StaticFragments extends FragmentShape[] = [], +> = FragmentDocumentFrom & { __phantom: In; }; /** * @internal */ -export function fragment( +export function partial( input: In, - staticFragments: FragmentDocumentForEach = [] as FragmentDocumentForEach, -): DynamicFragmentDocument { - return graphql(input, staticFragments) as DynamicFragmentDocument; + staticFragments?: StaticFragments, +): PartialFragment { + return graphql(input, staticFragments) as PartialFragment; } -/** - * @internal - */ +// https://github.com/0no-co/GraphQLSP/blob/6d9ce44d46dc6adbaf387ad5c96e4125570c3a94/packages/graphqlsp/src/ast/checks.ts#L26-L27 +partial.scalar = true; +partial.persisted = true; + +export type PartialFragmentOf< + Fragment extends FragmentShape, + DynamicFragments extends FragmentShape[], +> = Fragment extends PartialFragment + ? FragmentOf> + : never; + +export type DynamicDocument< + In extends string = string, + StaticFragments extends FragmentShape[] = [], +> = ( + dynamicFragments: DynamicFragments, +) => GetDocumentNode; + export type DynamicFragmentOf< - Document, - DynamicNodes extends AnyGqlNode[], -> = Document extends DynamicFragmentDocument - ? FragmentOf>> + Document extends DynamicDocument, + DynamicFragments extends FragmentShape[], +> = Document extends DynamicDocument + ? FragmentOf> : never; -export function assertTypename( - node: AnyGqlNode, - typename: Typename, -): asserts node is AnyGqlNode { - if (node.__typename !== typename) { - throw new InvariantError( - `Expected node to have typename "${typename}", but got "${node.__typename}"`, - ); - } +/** + * @internal + */ +export function dynamic( + input: In, + // biome-ignore lint/suspicious/noExplicitAny: simplicity + staticFragments: StaticFragments = [] as any, +): DynamicDocument { + return (dynamicFragments: DynamicFragments) => + graphql(input, staticFragments.concat(dynamicFragments) as FragmentShape[]); } + +// https://github.com/0no-co/GraphQLSP/blob/6d9ce44d46dc6adbaf387ad5c96e4125570c3a94/packages/graphqlsp/src/ast/checks.ts#L26-L27 +dynamic.scalar = true; +dynamic.persisted = true; + +/** + * @internal + */ +export type RequestOf = Document extends DynamicDocument + ? RequestOf> + : Document extends DocumentDecoration + ? Request + : never; diff --git a/packages/graphql/src/post.ts b/packages/graphql/src/post.ts index fced0f8a4..2289b2bb2 100644 --- a/packages/graphql/src/post.ts +++ b/packages/graphql/src/post.ts @@ -4,13 +4,13 @@ import { AccountPostReactionFragment, ActionInfoFragment, AnyPostFragment, + FullPostMetadataFragment, PaginatedResultInfoFragment, - PostMetadataFragment, SelfFundedTransactionRequestFragment, SponsoredTransactionRequestFragment, TransactionWillFailFragment, } from './fragments'; -import { type RequestOf, graphql } from './graphql'; +import { type RequestOf, dynamic, graphql } from './graphql'; const PostResponseFragment = graphql( `fragment PostResponse on PostResponse { @@ -74,21 +74,21 @@ export const EditPostMutation = graphql( ); export type EditPostRequest = RequestOf; -export const PostQuery = graphql( +export const postQuery = dynamic( `query Post($request: PostRequest!) { value: post(request: $request) { - ...AnyPost + ${'...AnyPost'} } }`, [AnyPostFragment], ); -export type PostRequest = RequestOf; +export type PostRequest = RequestOf; -export const PostsQuery = graphql( +export const postsQuery = dynamic( `query Posts($request: PostsRequest!) { value: posts(request: $request) { items { - ...AnyPost + ${'...AnyPost'} } pageInfo { ...PaginatedResultInfo @@ -97,7 +97,7 @@ export const PostsQuery = graphql( }`, [AnyPostFragment, PaginatedResultInfoFragment], ); -export type PostsRequest = RequestOf; +export type PostsRequest = RequestOf; export const PostActionsQuery = graphql( `query PostActions($request: PostActionsRequest!) { @@ -129,11 +129,11 @@ export const PostReactionsQuery = graphql( ); export type PostReactionsRequest = RequestOf; -export const PostBookmarksQuery = graphql( +export const postBookmarksQuery = dynamic( `query PostBookmarks($request: PostBookmarksRequest!) { value: postBookmarks(request: $request) { items { - ...AnyPost + ${'...AnyPost'} }, pageInfo { ...PaginatedResultInfo @@ -142,13 +142,13 @@ export const PostBookmarksQuery = graphql( }`, [AnyPostFragment, PaginatedResultInfoFragment], ); -export type PostBookmarksRequest = RequestOf; +export type PostBookmarksRequest = RequestOf; -export const PostReferencesQuery = graphql( +export const postReferencesQuery = dynamic( `query PostReferences($request: PostReferencesRequest!) { value: postReferences(request: $request) { items { - ...AnyPost + ${'...AnyPost'} }, pageInfo { ...PaginatedResultInfo @@ -157,7 +157,7 @@ export const PostReferencesQuery = graphql( }`, [AnyPostFragment, PaginatedResultInfoFragment], ); -export type PostReferencesRequest = RequestOf; +export type PostReferencesRequest = RequestOf; const AddReactionResultFragment = graphql( `fragment AddReactionResult on AddReactionResult { @@ -358,7 +358,7 @@ export const PostEditFragment = graphql( } timestamp }`, - [PostMetadataFragment], + [FullPostMetadataFragment], ); export type PostEdit = FragmentOf; diff --git a/packages/graphql/src/timeline.ts b/packages/graphql/src/timeline.ts index 3d5ac2815..8321c7448 100644 --- a/packages/graphql/src/timeline.ts +++ b/packages/graphql/src/timeline.ts @@ -1,53 +1,53 @@ import type { FragmentOf } from 'gql.tada'; -import { PaginatedResultInfoFragment, PostFragment } from './fragments'; -import { type RequestOf, graphql } from './graphql'; +import { PaginatedResultInfoFragment } from './fragments'; +import { type RequestOf, dynamic, partial } from './graphql'; -const TimelineItemFragment = graphql( +const timelineItemFragment = partial( `fragment TimelineItem on TimelineItem { __typename id primary { - ...Post + ${'...Post'} } comments { - ...Post + ${'...Post'} } reposts { - ...Post + ${'...Post'} } }`, - [PostFragment], + [], ); -export type TimelineItem = FragmentOf; +export type TimelineItem = FragmentOf; -export const TimelineQuery = graphql( +export const timelineQuery = dynamic( `query Timeline($request: TimelineRequest!) { value: timeline(request: $request) { __typename items { - ...TimelineItem + ${'...TimelineItem'} } pageInfo { ...PaginatedResultInfo } } }`, - [TimelineItemFragment, PaginatedResultInfoFragment], + [timelineItemFragment, PaginatedResultInfoFragment], ); -export type TimelineRequest = RequestOf; +export type TimelineRequest = RequestOf; -export const TimelineHighlightsQuery = graphql( +export const timelineHighlightsQuery = dynamic( `query TimelineHighlights($request: TimelineHighlightsRequest!) { value: timelineHighlights(request: $request) { __typename items { - ...Post + ${'...Post'} } pageInfo { ...PaginatedResultInfo } } }`, - [PostFragment, PaginatedResultInfoFragment], + [PaginatedResultInfoFragment], ); -export type TimelineHighlightsRequest = RequestOf; +export type TimelineHighlightsRequest = RequestOf;