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

Add refreshmetadata and refreshmetadatastatus #1089

Open
wants to merge 17 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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: 2 additions & 1 deletion packages/client/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ export * from './follow';
export * from './graph';
export * from './group';
export * from './health';
export * from './namespace';
export * from './metadata';
export * from './ml';
export * from './namespace';
export * from './notifications';
export * from './post';
export * from './posts';
Expand Down
51 changes: 51 additions & 0 deletions packages/client/src/actions/metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { type PostId, assertOk, invariant } from '@lens-protocol/types';
import { beforeAll, describe, expect, it } from 'vitest';

import {
createPublicClient,
loginAsAccountOwner,
postOnlyTextMetadata,
wallet,
} from '../test-utils';
import { handleOperationWith } from '../viem';
import { refreshMetadata, waitForMetadata } from './metadata';
import { post } from './post';
import { fetchPost } from './posts';

describe('Metadata refresh actions', () => {
const client = createPublicClient();
let postId: PostId;

beforeAll(async () => {
// Create a post with metadata
const resources = await postOnlyTextMetadata();
const result = await loginAsAccountOwner().andThen((sessionClient) =>
post(sessionClient, {
contentUri: resources.uri,
})
.andThen(handleOperationWith(wallet))
.andThen(sessionClient.waitForTransaction)
.andThen((tx) => fetchPost(client, { txHash: tx })),
);
assertOk(result);
invariant(result.value, 'Expected post to be defined and created');
expect(result.value.id).toBeDefined();
console.log(`Post created with id: ${result.value.id}`);
postId = result.value.id;
});

it('Possible to refreshMetadata and wait to be updated', async () => {
// TODO: add possibility to change metadata in the same URL and refresh later
// That feature will be available soon in the storage nodes
const newMetadata = await loginAsAccountOwner().andThen((sessionClient) =>
refreshMetadata(sessionClient, { post: postId }),
);
assertOk(newMetadata);
invariant(newMetadata.value, 'Expected to be defined');
console.log(`Metadata refreshed with id: ${newMetadata.value.id}`);

const result = await waitForMetadata(client, newMetadata.value.id);
assertOk(result);
expect(result.value).toEqual(newMetadata.value.id);
});
});
94 changes: 94 additions & 0 deletions packages/client/src/actions/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type {
RefreshMetadataRequest,
RefreshMetadataResult,
RefreshMetadataStatusResult,
} from '@lens-protocol/graphql';
import {
IndexingStatus,
RefreshMetadataMutation,
RefreshMetadataStatusQuery,
} from '@lens-protocol/graphql';
import { ResultAsync, type UUID } from '@lens-protocol/types';

import type { AnyClient, SessionClient } from '../clients';
import { MetadataIndexingError, type UnauthenticatedError, UnexpectedError } from '../errors';
import { delay } from '../utils';

/**
* Fetch the indexing status of metadata.
*
* ```ts
* const result = await refreshMetadataStatus(anyClient,
* uuid("a0a88a62-377f-46eb-a1ec-ca6597aef164")
* );
* ```
*
* @param client - Any Lens client.
* @param request - The query request.
* @returns The indexing status of the metadata.
*/
export function refreshMetadataStatus(
client: AnyClient,
request: UUID,
): ResultAsync<RefreshMetadataStatusResult, UnexpectedError> {
return client.query(RefreshMetadataStatusQuery, { request });
}

/**
* Refresh the metadata for a given entity.
*
* ```ts
* const result = await refreshMetadata(sessionClient, {
* post: postId('42'),
* });
* ```
*
* @param client - The session client.
* @param request - The mutation request.
* @returns - UUID to track the metadata refresh.
*/
export function refreshMetadata(
client: SessionClient,
request: RefreshMetadataRequest,
): ResultAsync<RefreshMetadataResult, UnauthenticatedError | UnexpectedError> {
return client.mutation(RefreshMetadataMutation, { request });
}

/**
* Given a metadata id, wait for the metadata to be either confirmed or rejected by the Lens API.
*
* @param client - Any Lens client.
* @param id - The metadata id to wait for.
* @returns The metadata id if the metadata was confirmed or an error if the transaction was rejected.
*/
export function waitForMetadata(
client: AnyClient,
id: UUID,
): ResultAsync<UUID, MetadataIndexingError | UnexpectedError> {
return ResultAsync.fromPromise(pollMetadataStatus(client, id), (err) => {
if (err instanceof MetadataIndexingError || err instanceof UnexpectedError) {
return err;
}
return UnexpectedError.from(err);
});
}

async function pollMetadataStatus(client: AnyClient, id: UUID): Promise<UUID> {
const startedAt = Date.now();
while (Date.now() - startedAt < client.context.environment.indexingTimeout) {
const result = await refreshMetadataStatus(client, id);
if (result.isErr()) {
throw UnexpectedError.from(result.error);
}
switch (result.value.status) {
case IndexingStatus.Finished:
return result.value.id;
case IndexingStatus.Failed:
throw MetadataIndexingError.from(result.value.reason);
case IndexingStatus.Pending:
await delay(client.context.environment.pollingInterval);
break;
}
}
throw MetadataIndexingError.from(`Timeout waiting for metadata ${id}`);
}
6 changes: 3 additions & 3 deletions packages/client/src/clients.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { AuthenticateMutation, ChallengeMutation, RefreshMutation } from '@lens-protocol/graphql';
import type {
AuthenticationChallenge,
ChallengeRequest,
SignedAuthChallenge,
StandardData,
SwitchAccountRequest,
} from '@lens-protocol/graphql';
import { AuthenticateMutation, ChallengeMutation, RefreshMutation } from '@lens-protocol/graphql';
import type { Credentials, IStorage } from '@lens-protocol/storage';
import { createCredentialsStorage } from '@lens-protocol/storage';
import {
Expand All @@ -25,10 +26,9 @@ import {
createClient,
fetchExchange,
} from '@urql/core';
import { type AuthConfig, authExchange } from '@urql/exchange-auth';
import { type Logger, getLogger } from 'loglevel';

import type { SwitchAccountRequest } from '@lens-protocol/graphql';
import { type AuthConfig, authExchange } from '@urql/exchange-auth';
import { type AuthenticatedUser, authenticatedUser } from './AuthenticatedUser';
import { revokeAuthentication, switchAccount, transactionStatus } from './actions';
import type { ClientConfig } from './config';
Expand Down
7 changes: 7 additions & 0 deletions packages/client/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ export class TransactionIndexingError extends ResultAwareError {
name = 'TransactionIndexingError' as const;
}

/**
* Error indicating metadata failed to index.
*/
export class MetadataIndexingError extends ResultAwareError {
name = 'MetadataIndexingError' as const;
}

/**
* Error indicating an operation was not executed due to a validation error.
* See the `cause` property for more information.
Expand Down
18 changes: 17 additions & 1 deletion packages/client/src/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import { chains } from '@lens-network/sdk/viem';
import { StorageClient, testnet as storageEnv } from '@lens-protocol/storage-node-client';
import { evmAddress } from '@lens-protocol/types';
import { http, type Account, type Transport, type WalletClient, createWalletClient } from 'viem';
import type { Account, Transport, WalletClient } from 'viem';
import { http, createWalletClient } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

import { ContentWarning, type TextOnlyOptions, textOnly } from '@lens-protocol/metadata';
import { GraphQLErrorCode, PublicClient, staging as apiEnv } from '.';

const pk = privateKeyToAccount(import.meta.env.PRIVATE_KEY);
Expand Down Expand Up @@ -73,3 +75,17 @@ export function createGraphQLErrorObject(code: GraphQLErrorCode) {
}

export const storageClient = StorageClient.create(storageEnv);

export function postOnlyTextMetadata(customMetadata?: TextOnlyOptions) {
const metadata =
customMetadata !== undefined
? customMetadata
: {
content: 'This is a post for testing purposes',
tags: ['test', 'lens', 'sdk'],
contentWarning: ContentWarning.SENSITIVE,
locale: 'en-US',
};

return storageClient.uploadAsJson(textOnly(metadata));
}
1 change: 1 addition & 0 deletions packages/graphql/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './graph';
export * from './graphql';
export * from './group';
export * from './health';
export * from './metadata';
export * from './ml';
export * from './namespace';
export * from './notifications';
Expand Down
40 changes: 40 additions & 0 deletions packages/graphql/src/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { FragmentOf } from 'gql.tada';
import { type RequestOf, graphql } from './graphql';

export const RefreshMetadataStatusResultFragment = graphql(
`fragment RefreshMetadataStatusResult on RefreshMetadataStatusResult {
__typename
id
status
reason
updatedAt
}`,
);
export type RefreshMetadataStatusResult = FragmentOf<typeof RefreshMetadataStatusResultFragment>;

export const RefreshMetadataStatusQuery = graphql(
`query RefreshMetadataStatus($request: UUID!) {
value: refreshMetadataStatus(id: $request) {
...RefreshMetadataStatusResult
}
}`,
[RefreshMetadataStatusResultFragment],
);

export const RefreshMetadataResultFragment = graphql(
`fragment RefreshMetadataResult on RefreshMetadataResult {
__typename
id
}`,
);
export type RefreshMetadataResult = FragmentOf<typeof RefreshMetadataResultFragment>;

export const RefreshMetadataMutation = graphql(
`mutation RefreshMetadata($request: EntityId!) {
value: refreshMetadata(request: $request){
...RefreshMetadataResult
}
}`,
[RefreshMetadataResultFragment],
);
export type RefreshMetadataRequest = RequestOf<typeof RefreshMetadataMutation>;
Loading