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

203 resources on last event query for rmrk2 #225

Merged
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/.vscode
/.idea
/node_modules
/lib
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ npx squid-typeorm-migration revert

## Setting up the project for resolvers

In case you just want to extend resolvers you don't index the whole project and just import the postgres database (last data 11.05.2022):
In case you just want to extend resolvers you don't index the whole project and just import the postgres database (last data 29.03.2023):

1. [Download the data](https://storage.googleapis.com/bright-meridian-316511-db-export/rubick.sql)
1. [Contact @vikiival on Discord](https://discord.gg/yfeumhRCuw)
2. `docker-compose up db`
3. `docker exec -it rubick-db-1 psql -U postgres -d postgres -c "CREATE DATABASE squid;"`
4. `docker exec -i rubick-db-1 psql -U postgres -d squid < rubick.sql`
Expand Down
13 changes: 7 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@subsquid/typeorm-migration": "0.1.6",
"@subsquid/typeorm-store": "0.2.2",
"dotenv": "^16.0.3",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"nanoid": "3.3.4",
"pg": "^8.10.0",
Expand Down
43 changes: 38 additions & 5 deletions src/server-extension/model/event.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Field, ObjectType } from 'type-graphql';
import { Field, Int, ObjectType } from "type-graphql";

@ObjectType()
export class EventEntity {
Expand Down Expand Up @@ -48,22 +48,55 @@ export class LastEventEntity {
@Field(() => String, { nullable: false })
value!: String;

@Field(() => String, { nullable: false, name: 'currentOwner' })
@Field(() => String, { nullable: false, name: "currentOwner" })
current_owner!: String;

@Field(() => String, { nullable: true })
image!: String;

@Field(() => String, { nullable: true, name: 'animationUrl' })
@Field(() => String, { nullable: true, name: "animationUrl" })
animation_url!: string | undefined | null;

@Field(() => String, { nullable: false, name: 'collectionId' })
@Field(() => String, { nullable: false, name: "collectionId" })
collection_id!: string;

@Field(() => String, { nullable: false, name: 'collectionName' })
@Field(() => String, { nullable: false, name: "collectionName" })
collection_name!: string;

@Field(() => [Resource], { nullable: true })
resources!: Resource[];

constructor(props: Partial<LastEventEntity>) {
Object.assign(this, props);
}
}

@ObjectType()
export class Resource {
@Field(() => String, { nullable: false })
id!: string;

@Field(() => String, { nullable: true })
src!: string;

@Field(() => String, { nullable: true })
metadata!: string;

@Field(() => String, { nullable: true })
slot!: string;

@Field(() => String, { nullable: true })
thumb!: string;

@Field(() => Int, { nullable: false })
priority!: number;

@Field(() => Boolean, { nullable: false })
pending!: boolean;

nftId!: string;

constructor(props: Partial<Resource>) {
Object.assign(this, props);
}
}
151 changes: 107 additions & 44 deletions src/server-extension/query/event.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,110 @@
export const buyEvent = `SELECT
COUNT(e.*) as count,
COALESCE(MAX(e.meta::decimal), 0) as max
FROM event e
LEFT JOIN nft_entity ne on ne.id = e.nft_id
WHERE e.interaction = 'BUY' AND ne.collection_id = $1;`;
export const buyEvent = `
SELECT COUNT(e.*) AS COUNT,
COALESCE(MAX(e.meta::decimal), 0) AS MAX
FROM event e
LEFT JOIN nft_entity ne ON ne.id = e.nft_id
WHERE e.interaction = 'BUY'
AND ne.collection_id = $1;
`;

export const collectionEventHistory = (
idList: string,
dateRange: string
) => `SELECT
ce.id as id,
DATE(e.timestamp),
count(e)
FROM nft_entity ne
JOIN collection_entity ce on ce.id = ne.collection_id
JOIN event e on e.nft_id = ne.id
WHERE e.interaction = 'BUY'
and ce.id in (${idList})
${dateRange}
GROUP BY ce.id, DATE(e.timestamp)
ORDER BY DATE(e.timestamp)`;
export const collectionEventHistory = (idList: string, dateRange: string) => `
SELECT ce.id AS id,
DATE(e.timestamp),
count(e)
FROM nft_entity ne
JOIN collection_entity ce ON ce.id = ne.collection_id
JOIN event e ON e.nft_id = ne.id
WHERE e.interaction = 'BUY'
AND ce.id in (${idList})
${dateRange}
GROUP BY ce.id,
DATE(e.timestamp)
ORDER BY DATE(e.timestamp)
`;

export const lastEventQuery = (whereCondition: string) => `SELECT
DISTINCT ne.id as id,
ne.name as name,
ne.issuer as issuer,
ne.metadata as metadata,
e.current_owner,
me.image as image,
me.animation_url,
MAX(e.timestamp) as timestamp,
MAX(e.meta::decimal) as value,
ne.collection_id as collection_id,
ce.name as collection_name

FROM event e
JOIN nft_entity ne on e.nft_id = ne.id
LEFT join metadata_entity me on me.id = ne.metadata
LEFT JOIN collection_entity ce on ne.collection_id = ce.id
WHERE
e.interaction = $1
AND ne.burned = false
export const lastEventQuery = (whereCondition: string) => `
SELECT DISTINCT ne.id AS id,
ne.name AS name,
ne.issuer AS issuer,
ne.metadata AS metadata,
e.current_owner,
me.image AS image,
me.animation_url,
MAX(e.timestamp) AS timestamp,
MAX(e.meta::decimal) AS value,
ne.collection_id AS collection_id,
ce.name AS collection_name
FROM event e
JOIN nft_entity ne ON e.nft_id = ne.id
LEFT JOIN metadata_entity me ON me.id = ne.metadata
LEFT JOIN collection_entity ce ON ne.collection_id = ce.id
WHERE e.interaction = $1
AND ne.burned = FALSE
${whereCondition}
GROUP BY ne.id, me.id, e.current_owner, me.image, ce.name
ORDER BY MAX(e.timestamp) DESC
LIMIT $2 OFFSET $3`;
GROUP BY ne.id,
me.id,
e.current_owner,
me.image,
ce.name
ORDER BY MAX(e.timestamp) DESC
LIMIT $2
OFFSET $3
`;

export const resourcesByNFT = (whereCondition: string) => `
SELECT r.id as id,
r.src as src,
r.metadata as metadata,
r.slot as slot,
r.thumb as thumb,
r.priority as priority,
r.pending as pending,
CASE
WHEN me.id IS NULL THEN NULL
ELSE json_strip_nulls(
json_build_object(
'attributes', me.attributes,
'name', me.name,
'description', me.description,
'id', me.id,
'animation_url', me.animation_url,
'type', me.type,
'image', me.image
)
)
END AS meta,
json_strip_nulls(
json_build_object(
'block_number', ne.block_number,
'burned', ne.burned,
'collection_id', ne.collection_id,
'created_at', ne.created_at,
'current_owner', ne.current_owner,
'emote_count', ne.emote_count,
'hash', ne.hash,
'id', ne.id,
'image', ne.image,
'instance', ne.instance,
'issuer', ne.issuer,
'media', ne.media,
'meta_id', ne.meta_id,
'metadata', ne.metadata,
'name', ne.name,
'parent_id', ne.parent_id,
'pending', ne.pending,
'price', ne.price,
'recipient', ne.recipient,
'royalty', ne.royalty,
'sn', ne.sn,
'transferable', ne.transferable,
'updated_at', ne.updated_at,
'version', ne.version
)
) as nft,
r.nft_id as nft_id
FROM resource r
LEFT JOIN nft_entity ne ON ne.id = r.nft_id
LEFT JOIN metadata_entity me ON me.id = r.meta_id
WHERE ${whereCondition}
GROUP BY r.id, ne.id, me.id
`;
64 changes: 44 additions & 20 deletions src/server-extension/resolvers/event.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,58 @@
import { Arg, Query, Resolver } from 'type-graphql'
import type { EntityManager } from 'typeorm'
import { NFTEntity } from '../../model/generated'
import { LastEventEntity } from '../model/event.model'
import { lastEventQuery } from '../query/event'
import { Arg, Info, Query, Resolver } from "type-graphql";
import { GraphQLResolveInfo } from "graphql";
import type { EntityManager } from "typeorm";
import { NFTEntity } from "../../model/generated";
import { LastEventEntity, Resource } from "../model/event.model";
import { lastEventQuery, resourcesByNFT } from "../query/event";
import { makeQuery, toSqlInParams } from "../utils";
import { Interaction } from '../../model'
import { Interaction } from "../../model";
import { passionQuery } from "../query/nft";
import { PassionFeedEntity } from '../model/passion.model'
import { PassionFeedEntity } from "../model/passion.model";
import { groupBy } from "lodash";

@Resolver()
type FieldName = {
name: {
value: string;
};
};

@Resolver((of) => LastEventEntity)
export class EventResolver {
constructor(private tx: () => Promise<EntityManager>) {}

@Query(() => [LastEventEntity])
async lastEvent(
@Arg('interaction', { nullable: true, defaultValue: Interaction.LIST }) interaction: Interaction,
@Arg('passionAccount', { nullable: true, }) account: string,
@Arg('limit', { nullable: true, defaultValue: 20 }) limit: number,
@Arg('offset', { nullable: true, defaultValue: 0 }) offset: number,
@Arg("interaction", { nullable: true, defaultValue: Interaction.LIST }) interaction: Interaction,
@Arg("passionAccount", { nullable: true }) account: string,
@Arg("limit", { nullable: true, defaultValue: 20 }) limit: number,
@Arg("offset", { nullable: true, defaultValue: 0 }) offset: number,
@Info() info: GraphQLResolveInfo
): Promise<[LastEventEntity]> {
const passionResult: [PassionFeedEntity] = await makeQuery(this.tx, NFTEntity, passionQuery, [account]);
const passionList = passionResult.map((passion) => passion.id);

const selectFromPassionList =
passionList && passionList.length > 0 ? `AND ne.issuer in (${toSqlInParams(passionList)})` : "";
let lastEvents: [LastEventEntity] = await makeQuery(this.tx, NFTEntity, lastEventQuery(selectFromPassionList), [
interaction,
limit,
offset,
]);

const passionResult: [PassionFeedEntity] = await makeQuery(this.tx, NFTEntity, passionQuery, [account])
const passionList = passionResult.map(passion => passion.id)
// TODO: Refactor this to use proper dataloader with FieldResolver, currently dataloader is not supported
// ref https://github.com/MichalLytek/type-graphql/issues/51
const isResourcesQueried = Object.values(info.fieldNodes[0]?.selectionSet?.selections as unknown[] as FieldName[])
.map((i) => i.name.value)
.includes("resources");

const selectFromPassionList = passionList && passionList.length > 0
? `AND ne.issuer in (${toSqlInParams(passionList)})`
: ''
const result: [LastEventEntity] = await makeQuery(this.tx, NFTEntity, lastEventQuery(selectFromPassionList), [interaction, limit, offset])
return result
}
if (isResourcesQueried) {
const whereCondition = `r.nft_id IN (${toSqlInParams(lastEvents.map((i) => String(i.id)))})`;
const resources: [Resource] = await makeQuery(this.tx, NFTEntity, resourcesByNFT(whereCondition));
const resourcesById = groupBy(resources, "nft_id");

lastEvents.map((event) => (event.resources = resourcesById[String(event.id)] ?? []));
}

return lastEvents;
}
}