From 0d3fd5ff8a8c05f62c17d30623d9be4bc35ba21f Mon Sep 17 00:00:00 2001 From: Cesare Naldi Date: Thu, 26 Dec 2024 14:02:36 +0100 Subject: [PATCH 1/5] feat: introduces custom Account fragment --- examples/custom-fragments/README.md | 3 ++ examples/custom-fragments/index.html | 18 ++++++++ examples/custom-fragments/index.ts | 54 +++++++++++++++++++++++ examples/custom-fragments/package.json | 19 ++++++++ examples/custom-fragments/tsconfig.json | 19 ++++++++ packages/client/src/actions/account.ts | 11 ++--- packages/client/src/clients.ts | 6 ++- packages/client/src/config.ts | 11 ++++- packages/client/src/context.ts | 19 ++++++-- packages/graphql/src/accounts/account.ts | 26 ++++++++--- packages/graphql/src/fragments/account.ts | 14 +++++- packages/graphql/src/graphql.ts | 9 +++- 12 files changed, 188 insertions(+), 21 deletions(-) create mode 100644 examples/custom-fragments/README.md create mode 100644 examples/custom-fragments/index.html create mode 100644 examples/custom-fragments/index.ts create mode 100644 examples/custom-fragments/package.json create mode 100644 examples/custom-fragments/tsconfig.json 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..c5daf26a7 --- /dev/null +++ b/examples/custom-fragments/index.ts @@ -0,0 +1,54 @@ +import 'viem/window'; + +import { + type FragmentOf, + PublicClient, + UsernameFragment, + evmAddress, + graphql, + testnet as protocolTestnet, +} from '@lens-protocol/client'; +import { fetchAccount } from '@lens-protocol/client/actions'; + +const MyAccountMetadataFragment = graphql( + `fragment AccountMetadata on AccountMetadata { + __typename + name + picture + }`, +); + +const MyAccountFragment = graphql( + `fragment Account on Account { + __typename + address + username { + ...Username + } + metadata { + ...AccountMetadata + } + }`, + [UsernameFragment, MyAccountMetadataFragment], +); + +type MyAccount = FragmentOf; + +const client = PublicClient.create({ + environment: protocolTestnet, + accountFragment: MyAccountFragment, +}); + +const account: MyAccount | null = await fetchAccount(client, { + address: evmAddress('0x57b62a1571F4F09CDB4C3d93dA542bfe142D9F81'), +}).match( + (account) => account, + (error) => { + throw error; + }, +); + +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..78c916750 100644 --- a/packages/client/src/actions/account.ts +++ b/packages/client/src/actions/account.ts @@ -33,7 +33,6 @@ import type { import { AccountFeedsStatsQuery, AccountGraphsStatsQuery, - AccountQuery, AccountStatsQuery, AccountsAvailableQuery, AccountsBlockedQuery, @@ -50,10 +49,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'; /** @@ -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 }); } /** 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..540bb8121 100644 --- a/packages/client/src/config.ts +++ b/packages/client/src/config.ts @@ -1,10 +1,12 @@ import type { EnvironmentConfig } from '@lens-protocol/env'; +import type { Account } 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 = { /** * The environment configuration to use (e.g. `mainnet`, `testnet`). */ @@ -27,11 +29,16 @@ 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; }; diff --git a/packages/client/src/context.ts b/packages/client/src/context.ts index 995d799f3..2765caa29 100644 --- a/packages/client/src/context.ts +++ b/packages/client/src/context.ts @@ -1,27 +1,40 @@ import type { EnvironmentConfig } from '@lens-protocol/env'; +import type { Account } from '@lens-protocol/graphql'; +import type { 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 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, + } as ContextFrom; } diff --git a/packages/graphql/src/accounts/account.ts b/packages/graphql/src/accounts/account.ts index 4be974022..abe986505 100644 --- a/packages/graphql/src/accounts/account.ts +++ b/packages/graphql/src/accounts/account.ts @@ -1,5 +1,6 @@ import type { FragmentOf } from 'gql.tada'; import { + type Account, AccountAvailableFragment, AccountBlockedFragment, AccountFragment, @@ -8,17 +9,28 @@ import { SponsoredTransactionRequestFragment, TransactionWillFailFragment, } from '../fragments'; -import { type RequestOf, graphql } from '../graphql'; - -export const AccountQuery = graphql( - `query Account($request: AccountRequest!) { +import { + type FragmentDocumentFor, + type RequestOf, + type RequestTypeOf, + type StandardDocumentNode, + graphql, +} from '../graphql'; + +export type AccountRequest = RequestTypeOf<'AccountRequest'>; + +export function accountQuery( + fragment: FragmentDocumentFor, +): StandardDocumentNode { + return graphql( + `query Account($request: AccountRequest!) { value: account(request: $request) { ...Account } }`, - [AccountFragment], -); -export type AccountRequest = RequestOf; + [fragment], + ) as StandardDocumentNode; +} export const AccountsQuery = graphql( `query Accounts($request: AccountsRequest!) { diff --git a/packages/graphql/src/fragments/account.ts b/packages/graphql/src/fragments/account.ts index 42e1a10ab..ad5642b32 100644 --- a/packages/graphql/src/fragments/account.ts +++ b/packages/graphql/src/fragments/account.ts @@ -54,6 +54,18 @@ export const AccountFragment = graphql( username{ ...Username } + }`, + [UsernameFragment], +); +export type Account = FragmentOf; + +export const FullAccountFragment = graphql( + `fragment Account on Account { + __typename + address + username{ + ...Username + } metadata { ...AccountMetadata } @@ -63,7 +75,7 @@ export const AccountFragment = graphql( }`, [AccountMetadataFragment, LoggedInAccountOperationsFragment, UsernameFragment], ); -export type Account = FragmentOf; +export type FullAccount = FragmentOf; const AccountManagerPermissionsFragment = graphql( `fragment AccountManagerPermissions on AccountManagerPermissions { diff --git a/packages/graphql/src/graphql.ts b/packages/graphql/src/graphql.ts index 299cbdd0a..00a66a240 100644 --- a/packages/graphql/src/graphql.ts +++ b/packages/graphql/src/graphql.ts @@ -164,6 +164,8 @@ export const graphql = initGraphQLTada<{ }; }>(); +export type { FragmentOf, TadaDocumentNode }; + /** * @internal */ @@ -205,7 +207,12 @@ export type FragmentDocumentFor = TGqlNode extends > : never; -export type RequestFrom = RequestOf>; +/** + * @internal + */ +export type RequestTypeOf = RequestOf< + GetDocumentNode<`query Named($request: ${Name}) {}`, FragmentShape[]> +>; // biome-ignore lint/suspicious/noExplicitAny: simplifies necessary type assertions export type StandardDocumentNode = TadaDocumentNode< From 7b878fcb0101ab1ff450b1d22267c9f6cea5bcc9 Mon Sep 17 00:00:00 2001 From: Cesare Naldi Date: Thu, 26 Dec 2024 14:53:43 +0100 Subject: [PATCH 2/5] feat: fetchAccounts support custom fragments --- examples/custom-fragments/index.ts | 17 +++++------------ packages/client/src/actions/account.ts | 18 ++++++++++++------ packages/graphql/src/accounts/account.ts | 18 +++++++++++------- packages/graphql/src/fragments/account.ts | 8 +------- 4 files changed, 29 insertions(+), 32 deletions(-) diff --git a/examples/custom-fragments/index.ts b/examples/custom-fragments/index.ts index c5daf26a7..0d9f3b3e2 100644 --- a/examples/custom-fragments/index.ts +++ b/examples/custom-fragments/index.ts @@ -3,33 +3,26 @@ import 'viem/window'; import { type FragmentOf, PublicClient, - UsernameFragment, evmAddress, graphql, testnet as protocolTestnet, } from '@lens-protocol/client'; import { fetchAccount } from '@lens-protocol/client/actions'; -const MyAccountMetadataFragment = graphql( - `fragment AccountMetadata on AccountMetadata { - __typename - name - picture - }`, -); - const MyAccountFragment = graphql( `fragment Account on Account { __typename address username { - ...Username + value } metadata { - ...AccountMetadata + __typename + name + picture } }`, - [UsernameFragment, MyAccountMetadataFragment], + [], ); type MyAccount = FragmentOf; diff --git a/packages/client/src/actions/account.ts b/packages/client/src/actions/account.ts index 78c916750..8b043fa55 100644 --- a/packages/client/src/actions/account.ts +++ b/packages/client/src/actions/account.ts @@ -37,7 +37,6 @@ import { AccountsAvailableQuery, AccountsBlockedQuery, AccountsBulkQuery, - AccountsQuery, BlockMutation, CreateAccountWithUsernameMutation, EnableSignlessMutation, @@ -50,6 +49,7 @@ import { UndoRecommendAccountMutation, UnmuteAccountMutation, accountQuery, + accountsQuery, } from '@lens-protocol/graphql'; import type { ResultAsync } from '@lens-protocol/types'; @@ -85,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/graphql/src/accounts/account.ts b/packages/graphql/src/accounts/account.ts index abe986505..1019bd162 100644 --- a/packages/graphql/src/accounts/account.ts +++ b/packages/graphql/src/accounts/account.ts @@ -1,4 +1,5 @@ import type { FragmentOf } from 'gql.tada'; +import type { Paginated } from '../common'; import { type Account, AccountAvailableFragment, @@ -18,7 +19,6 @@ import { } from '../graphql'; export type AccountRequest = RequestTypeOf<'AccountRequest'>; - export function accountQuery( fragment: FragmentDocumentFor, ): StandardDocumentNode { @@ -32,11 +32,15 @@ export function accountQuery( ) as StandardDocumentNode; } -export const AccountsQuery = graphql( - `query Accounts($request: AccountsRequest!) { +export type AccountsRequest = RequestTypeOf<'AccountsRequest'>; +export function accountsQuery( + fragment: FragmentDocumentFor, +): StandardDocumentNode, AccountsRequest> { + return graphql( + `query Accounts($request: AccountsRequest!) { value: accounts(request: $request) { __typename - items{ + items { ...Account } pageInfo { @@ -44,9 +48,9 @@ export const AccountsQuery = graphql( } } }`, - [AccountFragment, PaginatedResultInfoFragment], -); -export type AccountsRequest = RequestOf; + [fragment, PaginatedResultInfoFragment], + ) as StandardDocumentNode; +} export const AccountsBulkQuery = graphql( `query AccountsBulk($request: AccountsBulkRequest!) { diff --git a/packages/graphql/src/fragments/account.ts b/packages/graphql/src/fragments/account.ts index ad5642b32..112ed209d 100644 --- a/packages/graphql/src/fragments/account.ts +++ b/packages/graphql/src/fragments/account.ts @@ -48,14 +48,8 @@ export const AccountFragment = graphql( `fragment Account on Account { __typename address - owner - score - createdAt - username{ - ...Username - } }`, - [UsernameFragment], + [], ); export type Account = FragmentOf; From 531439fd154c079e3bb2594d7083963e4dd0d1e0 Mon Sep 17 00:00:00 2001 From: Cesare Naldi Date: Thu, 26 Dec 2024 15:08:17 +0100 Subject: [PATCH 3/5] fix: default context value for custom fragments --- examples/custom-fragments/index.ts | 8 +------- packages/client/src/context.ts | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/examples/custom-fragments/index.ts b/examples/custom-fragments/index.ts index 0d9f3b3e2..5d516eb87 100644 --- a/examples/custom-fragments/index.ts +++ b/examples/custom-fragments/index.ts @@ -22,7 +22,6 @@ const MyAccountFragment = graphql( picture } }`, - [], ); type MyAccount = FragmentOf; @@ -34,12 +33,7 @@ const client = PublicClient.create({ const account: MyAccount | null = await fetchAccount(client, { address: evmAddress('0x57b62a1571F4F09CDB4C3d93dA542bfe142D9F81'), -}).match( - (account) => account, - (error) => { - throw error; - }, -); +}).unwrapOr(null); export default [ `

${account?.username?.value}

`, diff --git a/packages/client/src/context.ts b/packages/client/src/context.ts index 2765caa29..7c0623a56 100644 --- a/packages/client/src/context.ts +++ b/packages/client/src/context.ts @@ -1,5 +1,5 @@ import type { EnvironmentConfig } from '@lens-protocol/env'; -import type { Account } from '@lens-protocol/graphql'; +import { type Account, AccountFragment } from '@lens-protocol/graphql'; import type { FragmentDocumentFor } from '@lens-protocol/graphql'; import { type IStorageProvider, InMemoryStorageProvider } from '@lens-protocol/storage'; import type { ClientConfig } from './config'; @@ -35,6 +35,6 @@ export function configureContext( debug: from.debug ?? false, origin: from.origin, storage: from.storage ?? new InMemoryStorageProvider(), - accountFragment: from.accountFragment, + accountFragment: from.accountFragment ?? AccountFragment, } as ContextFrom; } From 430c082e225fc0ea4a53e5bc3faf075e86be098c Mon Sep 17 00:00:00 2001 From: Cesare Naldi Date: Tue, 7 Jan 2025 15:47:12 +0100 Subject: [PATCH 4/5] feat: implements PostFields fragment injection --- packages/client/src/actions/account.ts | 4 +- packages/client/src/actions/authentication.ts | 10 +- packages/client/src/actions/posts.ts | 43 ++-- packages/client/src/actions/timeline.ts | 6 +- packages/client/src/config.ts | 15 +- packages/client/src/context.ts | 13 +- packages/client/testing-utils.ts | 3 + packages/graphql/src/accounts/account.ts | 42 +--- packages/graphql/src/authentication.ts | 27 +- packages/graphql/src/fragments/account.ts | 69 ++++-- packages/graphql/src/fragments/post.ts | 232 +++++++++++------- packages/graphql/src/graphql.ts | 163 +++++++----- packages/graphql/src/post.ts | 30 +-- packages/graphql/src/timeline.ts | 32 +-- 14 files changed, 427 insertions(+), 262 deletions(-) diff --git a/packages/client/src/actions/account.ts b/packages/client/src/actions/account.ts index 8b043fa55..3a2b9650b 100644 --- a/packages/client/src/actions/account.ts +++ b/packages/client/src/actions/account.ts @@ -76,7 +76,7 @@ export function fetchAccount( client: AnyClient>, request: AccountRequest, ): ResultAsync { - return client.query(accountQuery(client.context.accountFragment), { request }); + return client.query(accountQuery([client.context.accountFragment]), { request }); } /** @@ -102,7 +102,7 @@ export function fetchAccounts( client: AnyClient>, request: AccountsRequest = {}, ): ResultAsync | null, UnexpectedError> { - return client.query(accountsQuery(client.context.accountFragment), { request }); + 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..08fc62ea9 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, +export function fetchMeDetails( + client: SessionClient>, ): ResultAsync { - return client.query(MeQuery, {}); + 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/config.ts b/packages/client/src/config.ts index 540bb8121..2dadb38cc 100644 --- a/packages/client/src/config.ts +++ b/packages/client/src/config.ts @@ -1,12 +1,15 @@ import type { EnvironmentConfig } from '@lens-protocol/env'; -import type { Account } from '@lens-protocol/graphql'; +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`). */ @@ -40,5 +43,11 @@ export type ClientConfig = { * * @defaultValue {@link AccountFragment} */ - accountFragment?: FragmentDocumentFor; + 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 7c0623a56..538faafc0 100644 --- a/packages/client/src/context.ts +++ b/packages/client/src/context.ts @@ -1,19 +1,23 @@ import type { EnvironmentConfig } from '@lens-protocol/env'; -import { type Account, AccountFragment } from '@lens-protocol/graphql'; -import type { FragmentDocumentFor } from '@lens-protocol/graphql'; +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; + accountFragment: FragmentDocumentFor; + postFieldsFragment: FragmentDocumentFor; }; /** @@ -36,5 +40,6 @@ export function configureContext( 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 1019bd162..d09faa95c 100644 --- a/packages/graphql/src/accounts/account.ts +++ b/packages/graphql/src/accounts/account.ts @@ -1,7 +1,5 @@ import type { FragmentOf } from 'gql.tada'; -import type { Paginated } from '../common'; import { - type Account, AccountAvailableFragment, AccountBlockedFragment, AccountFragment, @@ -10,34 +8,20 @@ import { SponsoredTransactionRequestFragment, TransactionWillFailFragment, } from '../fragments'; -import { - type FragmentDocumentFor, - type RequestOf, - type RequestTypeOf, - type StandardDocumentNode, - graphql, -} from '../graphql'; - -export type AccountRequest = RequestTypeOf<'AccountRequest'>; -export function accountQuery( - fragment: FragmentDocumentFor, -): StandardDocumentNode { - return graphql( - `query Account($request: AccountRequest!) { +import { type RequestOf, dynamic, graphql } from '../graphql'; + +export const accountQuery = dynamic( + `query Account($request: AccountRequest!) { value: account(request: $request) { ...Account } }`, - [fragment], - ) as StandardDocumentNode; -} - -export type AccountsRequest = RequestTypeOf<'AccountsRequest'>; -export function accountsQuery( - fragment: FragmentDocumentFor, -): StandardDocumentNode, AccountsRequest> { - return graphql( - `query Accounts($request: AccountsRequest!) { + [], +); +export type AccountRequest = RequestOf; + +export const accountsQuery = dynamic( + `query Accounts($request: AccountsRequest!) { value: accounts(request: $request) { __typename items { @@ -48,9 +32,9 @@ export function accountsQuery( } } }`, - [fragment, PaginatedResultInfoFragment], - ) as StandardDocumentNode; -} + [PaginatedResultInfoFragment], +); +export type AccountsRequest = RequestOf; export const AccountsBulkQuery = graphql( `query AccountsBulk($request: AccountsBulkRequest!) { diff --git a/packages/graphql/src/authentication.ts b/packages/graphql/src/authentication.ts index 32a3ceb8b..7ea7be5e0 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, + AccountAvailableFragment, + type FullAccount, + 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 @@ -215,9 +227,16 @@ const MeResultFragment = graphql( }`, [AccountAvailableFragment], ); +export type MeDetails = PartialFragmentOf< + typeof MeResultFragment, + [FragmentDocumentFor] +>; + +type test = MeDetails; + export type MeResult = FragmentOf; -export const MeQuery = graphql( +export const meQuery = dynamic( `query Me { value: me { ...MeResult diff --git a/packages/graphql/src/fragments/account.ts b/packages/graphql/src/fragments/account.ts index 112ed209d..1bc188b5c 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'; @@ -49,14 +49,36 @@ export const AccountFragment = graphql( __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 + owner + score + createdAt username{ ...Username } @@ -69,7 +91,10 @@ export const FullAccountFragment = graphql( }`, [AccountMetadataFragment, LoggedInAccountOperationsFragment, UsernameFragment], ); -export type FullAccount = FragmentOf; +/** + * @deprecated Define your own FullAccountFragment instead using {@link graphql} and {@link FragmentOf}. + */ +export type FullAccount = FragmentOf; const AccountManagerPermissionsFragment = graphql( `fragment AccountManagerPermissions on AccountManagerPermissions { @@ -95,7 +120,7 @@ export const AccountManagerFragment = graphql( ); export type AccountManager = FragmentOf; -const AccountManagedFragment = graphql( +const AccountManagedFragment = partial( `fragment AccountManaged on AccountManaged { __typename addedAt @@ -106,27 +131,43 @@ const AccountManagedFragment = graphql( ...AccountManagerPermissions } }`, - [AccountManagerPermissionsFragment, AccountFragment], + [AccountManagerPermissionsFragment], +); +export type AccountManaged = PartialFragmentOf< + typeof AccountManagedFragment, + [FragmentDocumentFor] +>; + +const AccountOwnedFragment = partial( + `fragment AccountOwned on AccountOwned { + __typename + addedAt + account { + ...Account + } + }`, + [], ); -export type AccountManaged = FragmentOf; +export type AccountOwned = PartialFragmentOf< + typeof AccountOwnedFragment, + [FragmentDocumentFor] +>; -export const AccountAvailableFragment = graphql( +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..958e96ac3 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,128 +197,185 @@ export const LoggedInPostOperationsFragment = graphql( ); export type LoggedInPostOperations = FragmentOf; -export const ReferencedPostFragment = graphql( - `fragment ReferencedPost on Post { +export const PostStatsFragment = graphql( + `fragment PostStats on PostStats { + __typename + bookmarks + collects + comments + quotes + reactions + reposts + }`, +); +export type PostStats = FragmentOf; + +export const PostFieldsFragment = graphql( + `fragment PostFields on Post { __typename id - author { - ...Account - } - feed { - ...Feed - } 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 + timestamp + slug app { ...App } + feed { + ...Feed + } metadata { ...PostMetadata } - actions { - ...PostAction - } operations { ...LoggedInPostOperations } - } - `, + stats { + ...PostStats + } + }`, [ - AccountFragment, AppFragment, FeedFragment, - PostMetadataFragment, - PostActionFragment, LoggedInPostOperationsFragment, + FullPostMetadataFragment, + PostStatsFragment, ], ); +/** + * @deprecated Define your own FullPostFieldsFragment instead using {@link graphql} and {@link FragmentOf}. + */ +export type FullPostFields = FragmentOf; -export const PostStatsFragment = graphql( - `fragment PostStats on PostStats { - __typename - bookmarks - collects - comments - quotes - reactions - reposts +export const ReferencedPostFragment = partial( + `fragment ReferencedPost on Post { + ${'...PostFields'} + + author { + ...Account + } }`, ); -export type PostStats = FragmentOf; -export const PostFragment = graphql( +export type ReferencedPost< + TPostFields extends PostFields = PostFields, + TAccount extends Account = Account, +> = PartialFragmentOf< + typeof ReferencedPostFragment, + [FragmentDocumentFor, FragmentDocumentFor] +>; + +export const PostFragment = partial( `fragment Post on Post { - __typename - id + ${'...PostFields'} + author { ...Account } - feed { - ...Feed - } - timestamp - slug - stats { - ...PostStats - } - app { - ...App - } - metadata { - ...PostMetadata - } root { - ...ReferencedPost + ${'...ReferencedPost'} } quoteOf { - ...ReferencedPost + ${'...ReferencedPost'} } commentOn { - ...ReferencedPost + ${'...ReferencedPost'} } - actions { - ...PostAction - } - operations { - ...LoggedInPostOperations - } - } - `, - [ - AccountFragment, - AppFragment, - FeedFragment, - PostMetadataFragment, - PostActionFragment, - PostStatsFragment, - ReferencedPostFragment, - LoggedInPostOperationsFragment, - ], + }`, + [ReferencedPostFragment], ); -export type Post = FragmentOf; -// operations: LoggedInPostOperations -export const RepostFragment = graphql( +export type Post< + TPostFields extends PostFields = PostFields, + TAccount extends Account = Account, +> = PartialFragmentOf< + typeof PostFragment, + [FragmentDocumentFor, FragmentDocumentFor] +>; + +export const RepostFragment = partial( `fragment Repost on Repost { __typename id + author { + ...Account + } + isDeleted + timestamp + app { + ...App + } + repostOf { + ${'...PostFields'} + + author { + ...Account + } + root { + ${'...ReferencedPost'} + } + quoteOf { + ${'...ReferencedPost'} + } + commentOn { + ${'...ReferencedPost'} + } + } }`, - [], + [AppFragment, PostFragment], ); -export type Repost = FragmentOf; +export type Repost< + TPostFields extends PostFields = PostFields, + TAccount extends Account = Account, +> = PartialFragmentOf< + typeof RepostFragment, + [FragmentDocumentFor, FragmentDocumentFor] +>; -export const AnyPostFragment = graphql( +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 00a66a240..f7b237be5 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; @@ -166,16 +168,6 @@ export const graphql = initGraphQLTada<{ export type { FragmentOf, TadaDocumentNode }; -/** - * @internal - */ -export type RequestOf = Document extends DocumentDecoration< - unknown, - { request: infer Request } -> - ? Request - : never; - /** * @internal */ @@ -186,87 +178,128 @@ 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; + +type TypedSelectionSet = { __typename: TTypename }; /** * @internal */ -export type RequestTypeOf = RequestOf< - GetDocumentNode<`query Named($request: ${Name}) {}`, FragmentShape[]> +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; + } >; -// biome-ignore lint/suspicious/noExplicitAny: simplifies necessary type assertions -export type StandardDocumentNode = TadaDocumentNode< - StandardData, - { request: Request } ->; +/** + * 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; } +// 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 = < + DynamicFragments extends FragmentShape[], +>( + dynamicFragments: DynamicFragments, +) => GetDocumentNode; + /** * @internal */ -export type DynamicFragmentOf< - Document, - DynamicNodes extends AnyGqlNode[], -> = Document extends DynamicFragmentDocument - ? 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}"`, - ); - } +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; From 259d923166e24bb88678da9429de4cfe087d04f2 Mon Sep 17 00:00:00 2001 From: Cesare Naldi Date: Wed, 8 Jan 2025 10:47:47 +0100 Subject: [PATCH 5/5] WIP --- packages/client/src/actions/authentication.ts | 2 +- packages/graphql/src/accounts/account.ts | 2 +- packages/graphql/src/authentication.ts | 14 ++--- packages/graphql/src/fragments/account.ts | 8 +-- packages/graphql/src/fragments/post.ts | 59 +++++++++++-------- packages/graphql/src/graphql.ts | 14 ++++- 6 files changed, 55 insertions(+), 44 deletions(-) diff --git a/packages/client/src/actions/authentication.ts b/packages/client/src/actions/authentication.ts index 08fc62ea9..7cfba3d08 100644 --- a/packages/client/src/actions/authentication.ts +++ b/packages/client/src/actions/authentication.ts @@ -155,6 +155,6 @@ export function switchAccount( */ export function fetchMeDetails( client: SessionClient>, -): ResultAsync { +): ResultAsync, UnauthenticatedError | UnexpectedError> { return client.query(meQuery([client.context.accountFragment]), {}); } diff --git a/packages/graphql/src/accounts/account.ts b/packages/graphql/src/accounts/account.ts index d09faa95c..7caa73e58 100644 --- a/packages/graphql/src/accounts/account.ts +++ b/packages/graphql/src/accounts/account.ts @@ -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 7ea7be5e0..6dc6e2f08 100644 --- a/packages/graphql/src/authentication.ts +++ b/packages/graphql/src/authentication.ts @@ -1,8 +1,8 @@ import type { FragmentOf } from 'gql.tada'; import { type Account, + type AccountAvailable, AccountAvailableFragment, - type FullAccount, PaginatedResultInfoFragment, } from './fragments'; import { @@ -222,24 +222,20 @@ const MeResultFragment = partial( window } loggedInAs { - ...AccountAvailable + ${'...AccountAvailable'} } }`, [AccountAvailableFragment], ); -export type MeDetails = PartialFragmentOf< +export type MeResult = PartialFragmentOf< typeof MeResultFragment, - [FragmentDocumentFor] + [FragmentDocumentFor>] >; -type test = MeDetails; - -export type MeResult = FragmentOf; - 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 1bc188b5c..2c2074962 100644 --- a/packages/graphql/src/fragments/account.ts +++ b/packages/graphql/src/fragments/account.ts @@ -124,12 +124,12 @@ const AccountManagedFragment = partial( `fragment AccountManaged on AccountManaged { __typename addedAt - account { - ...Account - } permissions { ...AccountManagerPermissions } + account { + ...Account + } }`, [AccountManagerPermissionsFragment], ); @@ -146,8 +146,8 @@ const AccountOwnedFragment = partial( ...Account } }`, - [], ); + export type AccountOwned = PartialFragmentOf< typeof AccountOwnedFragment, [FragmentDocumentFor] diff --git a/packages/graphql/src/fragments/post.ts b/packages/graphql/src/fragments/post.ts index 958e96ac3..1a38badac 100644 --- a/packages/graphql/src/fragments/post.ts +++ b/packages/graphql/src/fragments/post.ts @@ -275,24 +275,6 @@ export const FullPostFieldsFragment = graphql( */ export type FullPostFields = FragmentOf; -export const ReferencedPostFragment = partial( - `fragment ReferencedPost on Post { - ${'...PostFields'} - - author { - ...Account - } - }`, -); - -export type ReferencedPost< - TPostFields extends PostFields = PostFields, - TAccount extends Account = Account, -> = PartialFragmentOf< - typeof ReferencedPostFragment, - [FragmentDocumentFor, FragmentDocumentFor] ->; - export const PostFragment = partial( `fragment Post on Post { ${'...PostFields'} @@ -301,16 +283,27 @@ export const PostFragment = partial( ...Account } root { - ${'...ReferencedPost'} + ${'...PostFields'} + + author { + ...Account + } } quoteOf { - ${'...ReferencedPost'} + ${'...PostFields'} + + author { + ...Account + } } commentOn { - ${'...ReferencedPost'} + ${'...PostFields'} + + author { + ...Account + } } }`, - [ReferencedPostFragment], ); export type Post< @@ -339,19 +332,33 @@ export const RepostFragment = partial( author { ...Account } + root { - ${'...ReferencedPost'} + ${'...PostFields'} + + author { + ...Account + } } quoteOf { - ${'...ReferencedPost'} + ${'...PostFields'} + + author { + ...Account + } } commentOn { - ${'...ReferencedPost'} + ${'...PostFields'} + + author { + ...Account + } } } }`, - [AppFragment, PostFragment], + [AppFragment], ); + export type Repost< TPostFields extends PostFields = PostFields, TAccount extends Account = Account, diff --git a/packages/graphql/src/graphql.ts b/packages/graphql/src/graphql.ts index f7b237be5..0ee9cd7fe 100644 --- a/packages/graphql/src/graphql.ts +++ b/packages/graphql/src/graphql.ts @@ -273,12 +273,20 @@ export type PartialFragmentOf< ? FragmentOf> : never; -export type DynamicDocument = < - DynamicFragments extends FragmentShape[], ->( +export type DynamicDocument< + In extends string = string, + StaticFragments extends FragmentShape[] = [], +> = ( dynamicFragments: DynamicFragments, ) => GetDocumentNode; +export type DynamicFragmentOf< + Document extends DynamicDocument, + DynamicFragments extends FragmentShape[], +> = Document extends DynamicDocument + ? FragmentOf> + : never; + /** * @internal */