Skip to content

Commit

Permalink
feat: introduces custom Account fragment
Browse files Browse the repository at this point in the history
  • Loading branch information
cesarenaldi committed Jan 6, 2025
1 parent f6db0b1 commit 0d3fd5f
Show file tree
Hide file tree
Showing 12 changed files with 188 additions and 21 deletions.
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>
54 changes: 54 additions & 0 deletions examples/custom-fragments/index.ts
Original file line number Diff line number Diff line change
@@ -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<typeof MyAccountFragment>;

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 [
`<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": ["./"]
}
11 changes: 6 additions & 5 deletions packages/client/src/actions/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import type {
import {
AccountFeedsStatsQuery,
AccountGraphsStatsQuery,
AccountQuery,
AccountStatsQuery,
AccountsAvailableQuery,
AccountsBlockedQuery,
Expand All @@ -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';

/**
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 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 } 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,
} as ContextFrom<TConfig>;
}
26 changes: 19 additions & 7 deletions packages/graphql/src/accounts/account.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { FragmentOf } from 'gql.tada';
import {
type Account,
AccountAvailableFragment,
AccountBlockedFragment,
AccountFragment,
Expand All @@ -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<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>;
[fragment],
) as StandardDocumentNode;
}

export const AccountsQuery = graphql(
`query Accounts($request: AccountsRequest!) {
Expand Down
14 changes: 13 additions & 1 deletion packages/graphql/src/fragments/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ export const AccountFragment = graphql(
username{
...Username
}
}`,
[UsernameFragment],
);
export type Account = FragmentOf<typeof AccountFragment>;

export const FullAccountFragment = graphql(
`fragment Account on Account {
__typename
address
username{
...Username
}
metadata {
...AccountMetadata
}
Expand All @@ -63,7 +75,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 @@ -164,6 +164,8 @@ export const graphql = initGraphQLTada<{
};
}>();

export type { FragmentOf, TadaDocumentNode };

/**
* @internal
*/
Expand Down Expand Up @@ -205,7 +207,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

0 comments on commit 0d3fd5f

Please sign in to comment.