Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: custom fragments support #1042

Open
wants to merge 3 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/custom-fragments/README.md
Original file line number Diff line number Diff line change
@@ -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)
18 changes: 18 additions & 0 deletions examples/custom-fragments/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
</head>
<body>
<h1>Custom Fragments</h1>
<div id="app">Loading...</div>
<script type="module">
import out from './index.ts';
document.querySelector('#app').innerHTML = Array.isArray(out)
? out.map((x) => `<div style="margin-bottom: 16px;">${x}</div>`).join('')
: out;
</script>
</body>
</html>
41 changes: 41 additions & 0 deletions examples/custom-fragments/index.ts
Original file line number Diff line number Diff line change
@@ -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<typeof MyAccountFragment>;

const client = PublicClient.create({
environment: protocolTestnet,
accountFragment: MyAccountFragment,
});

const account: MyAccount | null = await fetchAccount(client, {
address: evmAddress('0x57b62a1571F4F09CDB4C3d93dA542bfe142D9F81'),
}).unwrapOr(null);

export default [
`<h2>${account?.username?.value}</h2>`,
`<pre>${JSON.stringify(account, null, 2)}</pre>`,
];
19 changes: 19 additions & 0 deletions examples/custom-fragments/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
19 changes: 19 additions & 0 deletions examples/custom-fragments/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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": ["./"]
}
29 changes: 18 additions & 11 deletions packages/client/src/actions/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,10 @@ import type {
import {
AccountFeedsStatsQuery,
AccountGraphsStatsQuery,
AccountQuery,
AccountStatsQuery,
AccountsAvailableQuery,
AccountsBlockedQuery,
AccountsBulkQuery,
AccountsQuery,
BlockMutation,
CreateAccountWithUsernameMutation,
EnableSignlessMutation,
Expand All @@ -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';

/**
Expand All @@ -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<TAccount extends Account>(
client: AnyClient<Context<TAccount>>,
request: AccountRequest,
): ResultAsync<Account | null, UnexpectedError> {
return client.query(AccountQuery, { request });
): ResultAsync<TAccount | null, UnexpectedError> {
return client.query(accountQuery(client.context.accountFragment), { request });
}

/**
Expand All @@ -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<TAccount extends Account>(
client: AnyClient<Context<TAccount>>,
request: AccountsRequest = {},
): ResultAsync<Paginated<Account> | null, UnexpectedError> {
return client.query(AccountsQuery, { request });
): ResultAsync<Paginated<TAccount> | null, UnexpectedError> {
return client.query(accountsQuery(client.context.accountFragment), { request });
}

/**
Expand Down
6 changes: 4 additions & 2 deletions packages/client/src/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -151,7 +151,9 @@ export class PublicClient<TContext extends Context = Context> extends AbstractCl
* @param options - The options to configure the client.
* @returns The new instance of the client.
*/
static create(options: ClientConfig): PublicClient<Context> {
static create<TConfig extends ClientConfig>(
options: TConfig,
): PublicClient<ContextFrom<TConfig>> {
return new PublicClient(configureContext(options));
}

Expand Down
11 changes: 9 additions & 2 deletions packages/client/src/config.ts
Original file line number Diff line number Diff line change
@@ -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<TAccount extends Account = Account> = {
/**
* The environment configuration to use (e.g. `mainnet`, `testnet`).
*/
Expand All @@ -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<TAccount>;
};
19 changes: 16 additions & 3 deletions packages/client/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,40 @@
import type { EnvironmentConfig } from '@lens-protocol/env';
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';

/**
* @internal
*/
export type Context = {
export type Context<TAccount extends Account = Account> = {
environment: EnvironmentConfig;
cache: boolean;
debug: boolean;
origin?: string;
storage: IStorageProvider;
accountFragment: FragmentDocumentFor<TAccount>;
};

/**
* @internal
*/
export function configureContext(from: ClientConfig): Context {
export type ContextFrom<TConfig extends ClientConfig> = TConfig extends ClientConfig<infer TAccount>
? Context<TAccount>
: never;

/**
* @internal
*/
export function configureContext<TConfig extends ClientConfig>(
from: TConfig,
): ContextFrom<TConfig> {
return {
environment: from.environment,
cache: from.cache ?? false,
debug: from.debug ?? false,
origin: from.origin,
storage: from.storage ?? new InMemoryStorageProvider(),
};
accountFragment: from.accountFragment ?? AccountFragment,
} as ContextFrom<TConfig>;
}
44 changes: 30 additions & 14 deletions packages/graphql/src/accounts/account.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { FragmentOf } from 'gql.tada';
import type { Paginated } from '../common';
import {
type Account,
AccountAvailableFragment,
AccountBlockedFragment,
AccountFragment,
Expand All @@ -8,33 +10,47 @@ 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<TAccount extends Account>(
fragment: FragmentDocumentFor<TAccount>,
): StandardDocumentNode<TAccount | null, AccountRequest> {
return graphql(
`query Account($request: AccountRequest!) {
value: account(request: $request) {
...Account
}
}`,
[AccountFragment],
);
export type AccountRequest = RequestOf<typeof AccountQuery>;

export const AccountsQuery = graphql(
`query Accounts($request: AccountsRequest!) {
[fragment],
) as StandardDocumentNode;
}

export type AccountsRequest = RequestTypeOf<'AccountsRequest'>;
export function accountsQuery<TAccount extends Account>(
fragment: FragmentDocumentFor<TAccount>,
): StandardDocumentNode<Paginated<TAccount>, AccountsRequest> {
return graphql(
`query Accounts($request: AccountsRequest!) {
value: accounts(request: $request) {
__typename
items{
items {
...Account
}
pageInfo {
...PaginatedResultInfo
}
}
}`,
[AccountFragment, PaginatedResultInfoFragment],
);
export type AccountsRequest = RequestOf<typeof AccountsQuery>;
[fragment, PaginatedResultInfoFragment],
) as StandardDocumentNode;
}

export const AccountsBulkQuery = graphql(
`query AccountsBulk($request: AccountsBulkRequest!) {
Expand Down
11 changes: 10 additions & 1 deletion packages/graphql/src/fragments/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ export const AccountMetadataFragment = graphql(
export type AccountMetadata = FragmentOf<typeof AccountMetadataFragment>;

export const AccountFragment = graphql(
`fragment Account on Account {
__typename
address
}`,
[],
);
export type Account = FragmentOf<typeof AccountFragment>;

export const FullAccountFragment = graphql(
`fragment Account on Account {
__typename
address
Expand All @@ -60,7 +69,7 @@ export const AccountFragment = graphql(
}`,
[AccountMetadataFragment, LoggedInAccountOperationsFragment, UsernameFragment],
);
export type Account = FragmentOf<typeof AccountFragment>;
export type FullAccount = FragmentOf<typeof AccountFragment>;

const AccountManagerPermissionsFragment = graphql(
`fragment AccountManagerPermissions on AccountManagerPermissions {
Expand Down
9 changes: 8 additions & 1 deletion packages/graphql/src/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ export const graphql = initGraphQLTada<{
};
}>();

export type { FragmentOf, TadaDocumentNode };

/**
* @internal
*/
Expand Down Expand Up @@ -204,7 +206,12 @@ export type FragmentDocumentFor<TGqlNode extends AnyGqlNode> = TGqlNode extends
>
: never;

export type RequestFrom<In extends string> = RequestOf<GetDocumentNode<In, FragmentShape[]>>;
/**
* @internal
*/
export type RequestTypeOf<Name extends string> = RequestOf<
GetDocumentNode<`query Named($request: ${Name}) {}`, FragmentShape[]>
>;

// biome-ignore lint/suspicious/noExplicitAny: simplifies necessary type assertions
export type StandardDocumentNode<Value = any, Request = any> = TadaDocumentNode<
Expand Down
Loading