feat(route/rss3): RSS3 metadata parser (#16486)
* init * done Signed-off-by: Innei <i@innei.in> * chore: cleanup Signed-off-by: Innei <i@innei.in> * fix: format Signed-off-by: Innei <i@innei.in> * fix: pass through Signed-off-by: Innei <i@innei.in> * chore: add testcase Signed-off-by: Innei <i@innei.in> * fix: update Signed-off-by: Innei <i@innei.in> --------- Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
6ef47acf93
commit
2d82ddcddc
|
|
@ -1,6 +1,10 @@
|
|||
import { Route } from '@/types';
|
||||
/* eslint-disable default-case */
|
||||
import { Route, type DataItem } from '@/types';
|
||||
|
||||
import { camelcaseKeys } from '@/utils/camelcase-keys';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import type { Action } from '@rss3/sdk';
|
||||
import type { GetRSS3DataMetadata } from './interfaces/metadata';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/:account/:network?/:tag?',
|
||||
|
|
@ -132,17 +136,447 @@ async function handler(ctx) {
|
|||
return {
|
||||
title: `${account} activities`,
|
||||
link: 'https://rss3.io',
|
||||
item: data.map((item) => ({
|
||||
title: `New ${item.tag} ${item.type} action on ${item.network}`,
|
||||
description: `New ${item.tag} ${item.type} action on ${item.network}<br /><br />From: ${item.from}<br/>To: ${item.to}`,
|
||||
link: item.actions?.[0]?.related_urls?.[0],
|
||||
guid: item.id,
|
||||
author: [
|
||||
{
|
||||
name: item.owner,
|
||||
avatar: `https://cdn.stamp.fyi/avatar/eth:${item.owner}`,
|
||||
},
|
||||
],
|
||||
})),
|
||||
item: data.map((item) => {
|
||||
const content = parseItemActionToContent(camelcaseKeys(item.actions));
|
||||
|
||||
const description = `New ${item.tag} ${item.type} action on ${item.network}<br /><br />From: ${item.from}<br/>To: ${item.to}`;
|
||||
return {
|
||||
title: `New ${item.tag} ${item.type} action on ${item.network}`,
|
||||
description: content ? `${description}<br /><br />${content}` : description,
|
||||
link: item.actions?.[0]?.related_urls?.[0],
|
||||
guid: item.id,
|
||||
author: [
|
||||
{
|
||||
name: item.owner,
|
||||
avatar: `https://cdn.stamp.fyi/avatar/eth:${item.owner}`,
|
||||
},
|
||||
],
|
||||
|
||||
_extra: { raw: item },
|
||||
} as DataItem;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function parseItemActionToContent(actions: Action[]): string | undefined {
|
||||
if (!actions) {
|
||||
return;
|
||||
}
|
||||
|
||||
let joint = '';
|
||||
|
||||
for (const action of actions) {
|
||||
const metadata = action.metadata;
|
||||
if (!metadata) {
|
||||
continue;
|
||||
}
|
||||
const { tag } = action;
|
||||
switch (tag) {
|
||||
case 'social':
|
||||
joint += renderSocialTagContent(action);
|
||||
break;
|
||||
case 'collectible':
|
||||
joint += renderCollectibleTagContent(action);
|
||||
break;
|
||||
case 'metaverse':
|
||||
joint += renderMetaverseTagContent(action);
|
||||
|
||||
break;
|
||||
case 'exchange':
|
||||
joint += renderExchange(action);
|
||||
break;
|
||||
case 'transaction':
|
||||
joint += renderTransaction(action);
|
||||
break;
|
||||
}
|
||||
|
||||
joint += '<hr />';
|
||||
}
|
||||
|
||||
return joint;
|
||||
}
|
||||
|
||||
const renderTransaction = (action: Action) => {
|
||||
let joint = '';
|
||||
const { type } = action;
|
||||
const tag = 'transaction';
|
||||
switch (type) {
|
||||
case 'transfer':
|
||||
case 'burn':
|
||||
case 'mint':
|
||||
case 'approval': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([
|
||||
/* html */ `<h4>Transaction ${type.toUpperCase().at(0) + type.slice(1)}</h4>`,
|
||||
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
|
||||
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
|
||||
/* html */ `<p><strong>Standard:</strong> ${metadata.standard}</p>`,
|
||||
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
|
||||
/* html */ `<p><strong>Decimals:</strong> ${metadata.decimals}</p>`,
|
||||
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
case 'event': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([/* html */ `<h4>Transaction Event</h4>`, /* html */ `<p><strong>Block Hash:</strong> ${metadata.block.hash}</p>`, /* html */ `<p><strong>Transaction Hash:</strong> ${metadata.transaction.hash}</p>`]);
|
||||
break;
|
||||
}
|
||||
case 'bridge': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([
|
||||
/* html */ `<h4>Transaction Bridge</h4>`,
|
||||
/* html */ `<p><strong>Action:</strong> ${metadata.action}</p>`,
|
||||
/* html */ `<p><strong>Source Network:</strong> ${metadata.sourceNetwork}</p>`,
|
||||
/* html */ `<p><strong>Target Network:</strong> ${metadata.targetNetwork}</p>`,
|
||||
metadata.token && /* html */ `<p><strong>Token name:</strong> ${metadata.token.name}</p>`,
|
||||
metadata.token && /* html */ `<p><strong>Token Symbol:</strong> ${metadata.token.symbol}</p>`,
|
||||
metadata.token && /* html */ `<p><strong>Token Value:</strong> ${metadata.token.value}</p>`,
|
||||
metadata.token && /* html */ `<p><strong>Token Address:</strong> ${metadata.token.address}</p>`,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return buildSectionFooterHTML(joint, action);
|
||||
};
|
||||
|
||||
const renderExchange = (action: Action) => {
|
||||
let joint = '';
|
||||
const { type } = action;
|
||||
const tag = 'exchange';
|
||||
switch (type) {
|
||||
case 'liquidity': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([
|
||||
/* html */ `<h4>Exchange Liquidity</h4>`,
|
||||
/* html */ `<p><strong>Action:</strong> ${metadata.action}</p>`,
|
||||
/* html */ `<p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Address</th>
|
||||
<th>Value</th>
|
||||
<th>Name</th>
|
||||
<th>Symbol</th>
|
||||
<th>Decimals</th>
|
||||
<th>Standard</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${metadata.tokens.map(
|
||||
(token) => /* html */ `<tr>
|
||||
<td>${token.address}</td>
|
||||
<td>${token.value}</td>
|
||||
<td>${token.name}</td>
|
||||
<td>${token.symbol}</td>
|
||||
<td>${token.decimals}</td>
|
||||
<td>${token.standard}</td>
|
||||
</tr>`
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</p>`,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
case 'staking': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([
|
||||
/* html */ `<h4>Exchange Liquidity</h4>`,
|
||||
/* html */ `<p><strong>Action:</strong> ${metadata.action}</p>`,
|
||||
metadata.token &&
|
||||
/* html */ `<p>
|
||||
|
||||
<strong>Token:</strong>
|
||||
<ul>
|
||||
<li><strong>Address:</strong> ${metadata.token.address}</li>
|
||||
<li><strong>Value:</strong> ${metadata.token.value}</li>
|
||||
<li><strong>Name:</strong> ${metadata.token.name}</li>
|
||||
<li><strong>Symbol:</strong> ${metadata.token.symbol}</li>
|
||||
<li><strong>Decimals:</strong> ${metadata.token.decimals}</li>
|
||||
<li><strong>Standard:</strong> ${metadata.token.standard}</li>
|
||||
</ul>
|
||||
</p>`,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
case 'swap': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([
|
||||
/* html */ `<h4>Exchange Swap</h4>`,
|
||||
/* html */ metadata.from && `<p><strong>From:</strong> ${metadata.from.address}</p>`,
|
||||
/* html */ metadata.to && `<p><strong>To:</strong> ${metadata.to?.address}</p>`,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return buildSectionFooterHTML(joint, action);
|
||||
};
|
||||
|
||||
const renderMetaverseTagContent = (action: Action) => {
|
||||
let joint = '';
|
||||
const { from, to, type } = action;
|
||||
const tag = 'metaverse';
|
||||
switch (type) {
|
||||
case 'burn': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([
|
||||
/* html */ `<h4>Metaverse Burn</h4>`,
|
||||
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
|
||||
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
|
||||
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
|
||||
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
|
||||
/* html */ `<p>${from} --> ${to}</p>`,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
case 'trade': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([
|
||||
/* html */ `<h4>Metaverse Trade</h4>`,
|
||||
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
|
||||
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
|
||||
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
|
||||
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
|
||||
/* html */ `<p>${from} --> ${to}</p>`,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
case 'mint':
|
||||
{
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([
|
||||
/* html */ `<h4>Metaverse Mint</h4>`,
|
||||
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
|
||||
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
|
||||
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
|
||||
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
|
||||
/* html */ `<p>${from} --> ${to}</p>`,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
case 'transfer': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([
|
||||
/* html */ `<h4>Metaverse Transfer</h4>`,
|
||||
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
|
||||
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
|
||||
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
|
||||
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
|
||||
/* html */ `<p>${from} --> ${to}</p>`,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return buildSectionFooterHTML(joint, action);
|
||||
};
|
||||
const renderCollectibleTagContent = (action: Action) => {
|
||||
let joint = '';
|
||||
const { from, to, type } = action;
|
||||
const tag = 'collectible';
|
||||
switch (type) {
|
||||
case 'approval': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([
|
||||
/* html */ `<h4>Collectible Approval</h4>`,
|
||||
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
|
||||
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
|
||||
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
|
||||
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
|
||||
/* html */ `<p>${from} --> ${to}</p>`,
|
||||
]);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'burn': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([
|
||||
/* html */ `<h4>Collectible Burn</h4>`,
|
||||
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
|
||||
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
|
||||
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
|
||||
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
|
||||
/* html */ `<p>${from} --> ${to}</p>`,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
case 'trade': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([
|
||||
/* html */ `<h4>Collectible Trade</h4>`,
|
||||
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
|
||||
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
|
||||
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
|
||||
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
|
||||
/* html */ `<p>${from} --> ${to}</p>`,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
case 'mint':
|
||||
{
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([
|
||||
/* html */ `<h4>Collectible Mint</h4>`,
|
||||
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
|
||||
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
|
||||
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
|
||||
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
|
||||
/* html */ `<p>${from} --> ${to}</p>`,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
case 'transfer': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([
|
||||
/* html */ `<h4>Collectible Transfer</h4>`,
|
||||
/* html */ `<p><strong>Name:</strong> ${metadata.name}</p>`,
|
||||
/* html */ `<p><strong>Address:</strong> ${metadata.address}</p>`,
|
||||
/* html */ `<p><strong>Symbol:</strong> ${metadata.symbol}</p>`,
|
||||
/* html */ `<p><strong>Value:</strong> ${metadata.value}</p>`,
|
||||
/* html */ `<p>${from} --> ${to}</p>`,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return buildSectionFooterHTML(joint, action);
|
||||
};
|
||||
|
||||
const renderSocialTagContent = (action: Action) => {
|
||||
let joint = '';
|
||||
const { from, to, platform, type } = action;
|
||||
const tag = 'social';
|
||||
switch (type) {
|
||||
case 'profile': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
|
||||
joint += buildHTML([
|
||||
`<p><strong>Name:</strong> ${metadata.name}</p>`,
|
||||
`<p><strong>Handle:</strong> ${metadata.handle}</p>`,
|
||||
`<p><strong>Bio:</strong> ${metadata.bio}</p>`,
|
||||
`<p><strong>Platform:</strong> ${platform}</p>`,
|
||||
metadata.imageUri && `<img src="https://ipfs.io/ipfs/${metadata.imageUri.split('://')[1]}" alt="${metadata.name}" style="max-width:100%; height:auto;"/>`,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
case 'mint': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([`<h4>Social Mint</h4>`, `<p><strong>Title:</strong> ${metadata.title}</p>`, `<p>${from} --> ${to}</p>`]);
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([`<h4>Social Delete</h4>`, `<p><strong>Title:</strong> ${metadata.title}</p>`]);
|
||||
break;
|
||||
}
|
||||
case 'post': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([
|
||||
/* html*/ `<h4>Social Post</h4>`,
|
||||
/* html*/ `<p><strong>Title:</strong> ${metadata.title}</p>`,
|
||||
/* html*/ `<p><strong>Author:</strong> ${metadata.handle}</p>`,
|
||||
/* html*/ `<p><strong>Content:</strong> <pre>${metadata.body}</pre></p>`,
|
||||
/* html*/ `<p><strong>Platform:</strong> ${platform}</p>`,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
case 'comment': {
|
||||
const metadata = extractMetadata(tag, type, action);
|
||||
if (!metadata) {
|
||||
break;
|
||||
}
|
||||
joint += buildHTML([
|
||||
/* html*/ `<h4>Social Comment</h4>`,
|
||||
/* html*/ `<p><strong>Comment Anchor:</strong><a href="${metadata.authorUrl}" target="_blank">${metadata.handle}</a></p>`,
|
||||
metadata.target && /* html*/ `<p><strong>Comment Target:</strong> <a href="${metadata.targetUrl}" target="_blank">${metadata.target.title || metadata.targetUrl}</a></p>`,
|
||||
]);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'reward':
|
||||
case 'revise':
|
||||
case 'proxy':
|
||||
case 'share':
|
||||
break;
|
||||
}
|
||||
|
||||
return joint;
|
||||
};
|
||||
|
||||
function extractMetadata<T1 extends string, T2 extends string>(_tag: T1, _type: T2, data: any): GetRSS3DataMetadata<T1, T2> | null {
|
||||
const metadata = data.metadata;
|
||||
if (!metadata) {
|
||||
return null;
|
||||
}
|
||||
return camelcaseKeys(data.metadata) as GetRSS3DataMetadata<T1, T2>;
|
||||
}
|
||||
|
||||
function buildHTML(arr: (string | boolean | undefined | null)[]): string {
|
||||
return arr.filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
const buildSectionFooterHTML = (string: string, action: Action) =>
|
||||
buildHTML([
|
||||
string,
|
||||
!!action.platform && `<p><strong>Platform:</strong> ${action.platform}</p>`,
|
||||
|
||||
/* html */ `<p><strong>Related URLs:</strong>
|
||||
<ul><li>${action.relatedUrls.map((url) => `<a href="${url}" target="_blank">${url}</a>`).join('</li><li>')}</li></ul></p>`,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import {
|
||||
CollectibleApproval,
|
||||
CollectibleBurn,
|
||||
CollectibleMint,
|
||||
CollectibleTrade,
|
||||
CollectibleTransfer,
|
||||
ExchangeLiquidity,
|
||||
ExchangeStaking,
|
||||
ExchangeSwap,
|
||||
MetaverseBurn,
|
||||
MetaverseMint,
|
||||
MetaverseTrade,
|
||||
MetaverseTransfer,
|
||||
SocialComment,
|
||||
SocialDelete,
|
||||
SocialMint,
|
||||
SocialPost,
|
||||
SocialProfile,
|
||||
SocialProxy,
|
||||
SocialRevise,
|
||||
SocialReward,
|
||||
SocialShare,
|
||||
StakeStaking,
|
||||
StakeTransaction,
|
||||
StakerProfitSnapshot,
|
||||
TransactionApproval,
|
||||
TransactionBridge,
|
||||
TransactionBurn,
|
||||
TransactionEvent,
|
||||
TransactionMint,
|
||||
TransactionTransfer,
|
||||
} from '@rss3/sdk';
|
||||
export type RSS3DataModels = {
|
||||
CollectibleApproval: CollectibleApproval;
|
||||
CollectibleBurn: CollectibleBurn;
|
||||
CollectibleMint: CollectibleMint;
|
||||
CollectibleTrade: CollectibleTrade;
|
||||
CollectibleTransfer: CollectibleTransfer;
|
||||
MetaverseBurn: MetaverseBurn;
|
||||
MetaverseMint: MetaverseMint;
|
||||
MetaverseTrade: MetaverseTrade;
|
||||
MetaverseTransfer: MetaverseTransfer;
|
||||
SocialComment: SocialComment;
|
||||
SocialDelete: SocialDelete;
|
||||
SocialMint: SocialMint;
|
||||
SocialPost: SocialPost;
|
||||
SocialProfile: SocialProfile;
|
||||
SocialProxy: SocialProxy;
|
||||
SocialRevise: SocialRevise;
|
||||
SocialReward: SocialReward;
|
||||
SocialShare: SocialShare;
|
||||
StakeStaking: StakeStaking;
|
||||
StakeTransaction: StakeTransaction;
|
||||
StakerProfitSnapshot: StakerProfitSnapshot;
|
||||
TransactionApproval: TransactionApproval;
|
||||
TransactionBridge: TransactionBridge;
|
||||
TransactionBurn: TransactionBurn;
|
||||
TransactionEvent: TransactionEvent;
|
||||
TransactionMint: TransactionMint;
|
||||
TransactionTransfer: TransactionTransfer;
|
||||
ExchangeLiquidity: ExchangeLiquidity;
|
||||
ExchangeStaking: ExchangeStaking;
|
||||
ExchangeSwap: ExchangeSwap;
|
||||
};
|
||||
export type GetRSS3DataMetadata<FirstKey extends string, SecondKey extends string> = `${Capitalize<FirstKey>}${Capitalize<SecondKey>}` extends keyof RSS3DataModels
|
||||
? RSS3DataModels[`${Capitalize<FirstKey>}${Capitalize<SecondKey>}`]
|
||||
: null;
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { camelcase, camelcaseKeys } from './camelcase-keys';
|
||||
|
||||
describe('test camelcase keys', () => {
|
||||
it('case 1 normal', () => {
|
||||
const obj = {
|
||||
tool: 'too',
|
||||
tool_name: 'too_name',
|
||||
|
||||
a_b: 1,
|
||||
a: 1,
|
||||
b: {
|
||||
c_d: 1,
|
||||
},
|
||||
};
|
||||
|
||||
expect(camelcaseKeys(obj)).toStrictEqual({
|
||||
tool: 'too',
|
||||
toolName: 'too_name',
|
||||
|
||||
aB: 1,
|
||||
a: 1,
|
||||
b: {
|
||||
cD: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('case 2: key has number', () => {
|
||||
const obj = {
|
||||
b147da0eaecbea00aeb62055: {
|
||||
data: {},
|
||||
},
|
||||
a_c11ab_Ac: [
|
||||
{
|
||||
a_b: 1,
|
||||
},
|
||||
1,
|
||||
],
|
||||
};
|
||||
|
||||
expect(camelcaseKeys(obj)).toStrictEqual({
|
||||
b147da0eaecbea00aeb62055: {
|
||||
data: {},
|
||||
},
|
||||
aC11abAc: [
|
||||
{
|
||||
aB: 1,
|
||||
},
|
||||
1,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('case 3: not a object', () => {
|
||||
const value = 1;
|
||||
expect(camelcaseKeys(value)).toBe(value);
|
||||
});
|
||||
|
||||
it('case 4: nullable value', () => {
|
||||
let value = null as any;
|
||||
expect(camelcaseKeys(value)).toBe(value);
|
||||
|
||||
value = undefined;
|
||||
expect(camelcaseKeys(value)).toBe(value);
|
||||
|
||||
value = Number.NaN;
|
||||
expect(camelcaseKeys(value)).toBe(value);
|
||||
});
|
||||
|
||||
it('case 5: array', () => {
|
||||
const arr = [
|
||||
{
|
||||
a_b: 1,
|
||||
},
|
||||
null,
|
||||
undefined,
|
||||
+0,
|
||||
-0,
|
||||
Number.POSITIVE_INFINITY,
|
||||
{
|
||||
a_b: 1,
|
||||
},
|
||||
];
|
||||
|
||||
expect(camelcaseKeys(arr)).toStrictEqual([
|
||||
{
|
||||
aB: 1,
|
||||
},
|
||||
null,
|
||||
undefined,
|
||||
+0,
|
||||
-0,
|
||||
Number.POSITIVE_INFINITY,
|
||||
{
|
||||
aB: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('case 6: filter out mongo id', () => {
|
||||
const obj = {
|
||||
_id: '123',
|
||||
a_b: 1,
|
||||
collections: {
|
||||
posts: {
|
||||
'661bb93307d35005ba96731b': {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(camelcaseKeys(obj)).toStrictEqual({
|
||||
id: '123',
|
||||
aB: 1,
|
||||
collections: {
|
||||
posts: {
|
||||
'661bb93307d35005ba96731b': {},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('case 7: start with underscore should not camelcase', () => {
|
||||
expect(camelcase('_id')).toBe('id');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const isObject = (obj: any) => obj && typeof obj === 'object';
|
||||
const isPlainObject = (obj: any) => isObject(obj) && Object.prototype.toString.call(obj) === '[object Object]' && Object.getPrototypeOf(obj) === Object.prototype;
|
||||
|
||||
/**
|
||||
* A simple camelCase function that only handles strings, but not handling symbol, date, or other complex case.
|
||||
* If you need to handle more complex cases, please use camelcase-keys package.
|
||||
*/
|
||||
export const camelcaseKeys = <T = any>(obj: any): T => {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((x) => camelcaseKeys(x)) as any;
|
||||
}
|
||||
|
||||
if (isPlainObject(obj)) {
|
||||
return Object.keys(obj).reduce((result: any, key) => {
|
||||
const nextKey = isMongoId(key) ? key : camelcase(key);
|
||||
result[nextKey] = camelcaseKeys(obj[key]);
|
||||
return result;
|
||||
}, {}) as any;
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
export function camelcase(str: string) {
|
||||
return str.replace(/^_+/, '').replaceAll(/([_-][a-z])/gi, ($1) => $1.toUpperCase().replace('-', '').replace('_', ''));
|
||||
}
|
||||
const isMongoId = (id: string) => id.length === 24 && /^[\dA-Fa-f]{24}$/.test(id);
|
||||
|
|
@ -61,6 +61,7 @@
|
|||
"@opentelemetry/sdk-trace-base": "1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "1.25.1",
|
||||
"@postlight/parser": "2.2.3",
|
||||
"@rss3/sdk": "0.0.13",
|
||||
"@scalar/hono-api-reference": "0.5.141",
|
||||
"@sentry/node": "7.116.0",
|
||||
"@tonyrl/rand-user-agent": "2.0.74",
|
||||
|
|
@ -196,4 +197,4 @@
|
|||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -41,6 +41,9 @@ importers:
|
|||
'@postlight/parser':
|
||||
specifier: 2.2.3
|
||||
version: 2.2.3
|
||||
'@rss3/sdk':
|
||||
specifier: 0.0.13
|
||||
version: 0.0.13
|
||||
'@scalar/hono-api-reference':
|
||||
specifier: 0.5.141
|
||||
version: 0.5.141(hono@4.5.7)
|
||||
|
|
@ -1740,6 +1743,15 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rss3/api-core@0.0.13':
|
||||
resolution: {integrity: sha512-5CRVS0FpCZ8YiS/ttHM0ZxPxM05+WkcEPE7a5klX9NvULbDCISdpYQkiUkdVjZw+hPD7MCtlvBc9tFqTQKojJw==}
|
||||
|
||||
'@rss3/api-utils@0.0.13':
|
||||
resolution: {integrity: sha512-ucN9myX/E0DlA3Q+KkpCD/HwpB6fQSMlaHqRhDDV3tuyYrQnP/PjRKpGibx1Sz2x519SJNnsfyDL931H00iDJA==}
|
||||
|
||||
'@rss3/sdk@0.0.13':
|
||||
resolution: {integrity: sha512-5QcC9NeMiKeay/H1JEGlG2ZtmKhZxlyLTA5f6dcIdGZUrSNk4DCM/J+8u0ED73bMQupi6xnUm+dIJdn6A1kHyA==}
|
||||
|
||||
'@scalar/hono-api-reference@0.5.141':
|
||||
resolution: {integrity: sha512-EFGgjf91RRRlyOJgZfCH/aiD8UXhgIeFdNEdHQZF20UoJr5NvugKUsK3YYm8StkJ3Q7ApXm9ASnkeU2oiGgbnA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -4445,6 +4457,12 @@ packages:
|
|||
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
openapi-fetch@0.11.1:
|
||||
resolution: {integrity: sha512-WtDQsrvxjXuCmo6u6WMQPfUaya8cLfL+ZCaXorPo9MMumqlU/Km/SrCXsEcJH234D4iykOkvJ6Q/iWBzK7+3rA==}
|
||||
|
||||
openapi-typescript-helpers@0.0.12:
|
||||
resolution: {integrity: sha512-FO+5kTWO6KDutigamr2MRwciYkAUYhqdctlyVRrQOe2uxif2/O2+GcS07jNnP36AUK6ubSsGu3GeBiYIc6eQzA==}
|
||||
|
||||
openapi3-ts@4.3.3:
|
||||
resolution: {integrity: sha512-LKkzBGJcZ6wdvkKGMoSvpK+0cbN5Xc3XuYkJskO+vjEQWJgs1kgtyUk0pjf8KwPuysv323Er62F5P17XQl96Qg==}
|
||||
|
||||
|
|
@ -5408,6 +5426,9 @@ packages:
|
|||
peerDependencies:
|
||||
typescript: '>=4.2.0'
|
||||
|
||||
ts-case-convert@2.0.7:
|
||||
resolution: {integrity: sha512-Kqj8wrkuduWsKUOUNRczrkdHCDt4ZNNd6HKjVw42EnMIGHQUABS4pqfy0acETVLwUTppc1fzo/yi11+uMTaqzw==}
|
||||
|
||||
ts-custom-error@2.2.2:
|
||||
resolution: {integrity: sha512-I0FEdfdatDjeigRqh1JFj67bcIKyRNm12UVGheBjs2pXgyELg2xeiQLVaWu1pVmNGXZVnz/fvycSU41moBIpOg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
|
@ -7303,6 +7324,21 @@ snapshots:
|
|||
'@rollup/rollup-win32-x64-msvc@4.21.0':
|
||||
optional: true
|
||||
|
||||
'@rss3/api-core@0.0.13':
|
||||
dependencies:
|
||||
openapi-fetch: 0.11.1
|
||||
ts-case-convert: 2.0.7
|
||||
type-fest: 4.25.0
|
||||
|
||||
'@rss3/api-utils@0.0.13':
|
||||
dependencies:
|
||||
'@rss3/api-core': 0.0.13
|
||||
|
||||
'@rss3/sdk@0.0.13':
|
||||
dependencies:
|
||||
'@rss3/api-core': 0.0.13
|
||||
'@rss3/api-utils': 0.0.13
|
||||
|
||||
'@scalar/hono-api-reference@0.5.141(hono@4.5.7)':
|
||||
dependencies:
|
||||
'@scalar/types': 0.0.3
|
||||
|
|
@ -10402,6 +10438,12 @@ snapshots:
|
|||
dependencies:
|
||||
mimic-function: 5.0.1
|
||||
|
||||
openapi-fetch@0.11.1:
|
||||
dependencies:
|
||||
openapi-typescript-helpers: 0.0.12
|
||||
|
||||
openapi-typescript-helpers@0.0.12: {}
|
||||
|
||||
openapi3-ts@4.3.3:
|
||||
dependencies:
|
||||
yaml: 2.5.0
|
||||
|
|
@ -11443,6 +11485,8 @@ snapshots:
|
|||
dependencies:
|
||||
typescript: 5.5.4
|
||||
|
||||
ts-case-convert@2.0.7: {}
|
||||
|
||||
ts-custom-error@2.2.2: {}
|
||||
|
||||
ts-custom-error@3.3.1: {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue