Skip to content

Commit

Permalink
Merge pull request #1030 from multiversx/SERVICES-1841-extend-collect…
Browse files Browse the repository at this point in the history
…ion-analytics

Services 1841 extend collection analytics
  • Loading branch information
danielailie authored Sep 26, 2023
2 parents 5425f61 + 00d689b commit f3266c1
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 17 deletions.
6 changes: 6 additions & 0 deletions schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ input AddLikeArgs {
type AnalyticsAggregateValue {
avg: Float
count: Float
marketplacesData: [MarketplaceData]
max: Float
min: Float
series: String
Expand Down Expand Up @@ -839,6 +840,11 @@ type Marketplace {
url: String!
}

type MarketplaceData {
key: String!
value: Float
}

type MarketplaceEdge {
cursor: String
node: Marketplace
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { computeTimeInterval } from 'src/utils/analytics.utils';
import { AnalyticsArgs } from './entities/analytics.query';
import { FloorPriceDaily, SumDaily } from './entities/sum-daily.entity';
import { FloorPriceDaily, SumDaily, SumMarketplaceDaily } from './entities/sum-daily.entity';
import { AnalyticsAggregateValue } from 'src/modules/analytics/models/analytics-aggregate-value';

@Injectable()
export class AnalyticsDataGetterService {
constructor(
@InjectRepository(SumDaily, 'timescaledb')
private readonly sumDaily: Repository<SumDaily>,
@InjectRepository(SumMarketplaceDaily, 'timescaledb')
private readonly sumMarketplaceDaily: Repository<SumMarketplaceDaily>,
@InjectRepository(FloorPriceDaily, 'timescaledb')
private readonly floorPriceDaily: Repository<FloorPriceDaily>,
) {}
Expand All @@ -37,23 +39,26 @@ export class AnalyticsDataGetterService {
return [[new AnalyticsAggregateValue({ value: 0, series: series })], 1];
}

return [response?.map((row) => AnalyticsAggregateValue.fromTimescaleObjext(row)) ?? [], count ?? 0];
return [response?.map((row) => AnalyticsAggregateValue.fromTimescaleObject(row)) ?? [], count ?? 0];
}

async getVolumeData({ time, series, metric, start }: AnalyticsArgs): Promise<AnalyticsAggregateValue[]> {
async getVolumeDataWithMarketplaces({ time, series, metric, start }: AnalyticsArgs): Promise<AnalyticsAggregateValue[]> {
const [startDate, endDate] = computeTimeInterval(time, start);
const query = await this.sumDaily
const query = await this.sumMarketplaceDaily
.createQueryBuilder()
.select("time_bucket_gapfill('1 day', time) as timestamp")
.addSelect('sum(sum) as value')
.addSelect(
'sum(sum) as value, sum(xoxno) as xoxno, sum(frameit) as frameit, sum(elrondapes) as elrondapes, sum(deadrare) as deadrare, sum(hoghomies) as hoghomies, sum(elrondnftswap) as elrondnftswap, sum(aquaverse) as aquaverse, sum(holoride) as holoride, sum(eneftor) as eneftor,sum(ici) as ici ',
)
.where('key = :metric', { metric })
.andWhere('series = :series', { series })
.andWhere(endDate ? 'time BETWEEN :startDate AND :endDate' : 'time >= :startDate', { startDate, endDate })
.orderBy('timestamp', 'ASC')
.groupBy('timestamp')
.getRawMany();

return query?.map((row) => AnalyticsAggregateValue.fromTimescaleObjext(row)) ?? [];
return query?.map((row) => AnalyticsAggregateValue.fromTimescaleObjectWithMarketplaces(row)) ?? [];
}

async getFloorPriceData({ time, series, metric, start }: AnalyticsArgs): Promise<AnalyticsAggregateValue[]> {
Expand All @@ -69,6 +74,6 @@ export class AnalyticsDataGetterService {
.groupBy('timestamp')
.getRawMany();

return query?.map((row) => AnalyticsAggregateValue.fromTimescaleObjext(row)) ?? [];
return query?.map((row) => AnalyticsAggregateValue.fromTimescaleObject(row)) ?? [];
}
}
61 changes: 61 additions & 0 deletions src/common/persistence/timescaledb/entities/sum-daily.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,67 @@ export class SumDaily {
}
}

@ViewEntity({
expression: `
SELECT
time_bucket('1 day', timestamp) AS time, series, key,
SUM(CASE WHEN "marketplaceKey" ='frameit' THEN value ELSE 0 END) as frameit,
SUM(CASE WHEN "marketplaceKey" ='xoxno' THEN value ELSE 0 END) as xoxno,
SUM(CASE WHEN "marketplaceKey" ='elrondapes' THEN value ELSE 0 END) as elrondapes,
SUM(CASE WHEN "marketplaceKey" ='deadrare' THEN value ELSE 0 END) as deadrare,
SUM(CASE WHEN "marketplaceKey" ='hoghomies' THEN value ELSE 0 END) as hoghomies,
SUM(CASE WHEN "marketplaceKey" ='elrondnftswap' THEN value ELSE 0 END) as elrondnftswap,
SUM(CASE WHEN "marketplaceKey" ='aquaverse' THEN value ELSE 0 END) as aquaverse,
SUM(CASE WHEN "marketplaceKey" ='holoride' THEN value ELSE 0 END) as holoride,
SUM(CASE WHEN "marketplaceKey" ='eneftor' THEN value ELSE 0 END) as eneftor,
SUM(CASE WHEN "marketplaceKey" ='ici' THEN value ELSE 0 END) as ici,
sum(value) AS sum
FROM "hyper_nfts_analytics"
WHERE key = 'volumeUSD'
GROUP BY time, series, key;
`,
materialized: true,
name: 'nfts_sum_marketplace_daily',
})
export class SumMarketplaceDaily {
@ViewColumn()
@PrimaryColumn()
time: Date = new Date();

@ViewColumn()
sum = '0';
@ViewColumn()
frameit = '0';
@ViewColumn()
xoxno = '0';
@ViewColumn()
elrondapes = '0';
@ViewColumn()
deadrare = '0';
@ViewColumn()
hoghomies = '0';
@ViewColumn()
elrondnftswap = '0';
@ViewColumn()
aquaverse = '0';
@ViewColumn()
holoride = '0';
@ViewColumn()
eneftor = '0';
@ViewColumn()
ici = '0';

@ViewColumn()
series?: string;

@ViewColumn()
key?: string;

constructor(init?: Partial<SumDaily>) {
Object.assign(this, init);
}
}

@ViewEntity({
expression: `
SELECT
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddDailyViewWithMarketplaceInfo1695294912071 implements MigrationInterface {
name = 'AddDailyViewWithMarketplaceInfo1695294912071';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE MATERIALIZED VIEW "nfts_sum_marketplace_daily" WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 day', timestamp) AS time, series, key,
SUM(CASE WHEN "marketplaceKey" ='frameit' THEN value ELSE 0 END) as frameit,
SUM(CASE WHEN "marketplaceKey" ='xoxno' THEN value ELSE 0 END) as xoxno,
SUM(CASE WHEN "marketplaceKey" ='elrondapes' THEN value ELSE 0 END) as elrondapes,
SUM(CASE WHEN "marketplaceKey" ='deadrare' THEN value ELSE 0 END) as deadrare,
SUM(CASE WHEN "marketplaceKey" ='hoghomies' THEN value ELSE 0 END) as hoghomies,
SUM(CASE WHEN "marketplaceKey" ='elrondnftswap' THEN value ELSE 0 END) as elrondnftswap,
SUM(CASE WHEN "marketplaceKey" ='aquaverse' THEN value ELSE 0 END) as aquaverse,
SUM(CASE WHEN "marketplaceKey" ='holoride' THEN value ELSE 0 END) as holoride,
SUM(CASE WHEN "marketplaceKey" ='eneftor' THEN value ELSE 0 END) as eneftor,
SUM(CASE WHEN "marketplaceKey" ='ici' THEN value ELSE 0 END) as ici,
sum(value) AS sum
FROM "hyper_nfts_analytics"
WHERE key = 'volumeUSD'
GROUP BY time, series, key;
`);
await queryRunner.query(
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`,
[
'public',
'MATERIALIZED_VIEW',
'nfts_sum_marketplace_daily',
"SELECT\n time_bucket('1 day', timestamp) AS time, series, key,\n SUM(CASE WHEN \"marketplaceKey\" ='frameit' THEN value ELSE 0 END) as frameit,\n SUM(CASE WHEN \"marketplaceKey\" ='xoxno' THEN value ELSE 0 END) as xoxno,\n SUM(CASE WHEN \"marketplaceKey\" ='elrondapes' THEN value ELSE 0 END) as elrondapes,\n SUM(CASE WHEN \"marketplaceKey\" ='deadrare' THEN value ELSE 0 END) as deadrare,\n SUM(CASE WHEN \"marketplaceKey\" ='hoghomies' THEN value ELSE 0 END) as hoghomies,\n SUM(CASE WHEN \"marketplaceKey\" ='elrondnftswap' THEN value ELSE 0 END) as elrondnftswap,\n SUM(CASE WHEN \"marketplaceKey\" ='aquaverse' THEN value ELSE 0 END) as aquaverse,\n SUM(CASE WHEN \"marketplaceKey\" ='holoride' THEN value ELSE 0 END) as holoride,\n SUM(CASE WHEN \"marketplaceKey\" ='eneftor' THEN value ELSE 0 END) as eneftor,\n SUM(CASE WHEN \"marketplaceKey\" ='ici' THEN value ELSE 0 END) as ici,\n sum(value) AS sum\n FROM \"hyper_nfts_analytics\"\n WHERE key = 'volumeUSD'\n GROUP BY time, series, key;",
],
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, [
'MATERIALIZED_VIEW',
'nfts_sum_marketplace_daily',
'public',
]);
await queryRunner.query(`DROP MATERIALIZED VIEW "nfts_sum_marketplace_daily"`);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AnalyticsDataGetterService } from '../analytics-data.getter.service';
import { FloorPriceDaily, SumDaily } from '../entities/sum-daily.entity';
import { FloorPriceDaily, SumDaily, SumMarketplaceDaily } from '../entities/sum-daily.entity';
import { getRepositoryToken } from '@nestjs/typeorm';
import { AnalyticsAggregateValue } from 'src/modules/analytics/models/analytics-aggregate-value';

Expand All @@ -24,6 +24,11 @@ describe('Analytics Data Getter Service', () => {
provide: getRepositoryToken(SumDaily, 'timescaledb'),
useFactory: () => ({}),
},
{
provide: getRepositoryToken(SumMarketplaceDaily, 'timescaledb'),
useFactory: () => ({}),
},

{
provide: getRepositoryToken(FloorPriceDaily, 'timescaledb'),
useFactory: () => ({}),
Expand Down Expand Up @@ -175,10 +180,10 @@ describe('Analytics Data Getter Service', () => {
});
});

describe('getVolumeData', () => {
describe('getVolumeDataWithMarketplaces', () => {
it('returns empty list when no data for specific collection', async () => {
const getRawMany = jest.fn().mockReturnValueOnce([]);
const sumDailyRepository = module.get(getRepositoryToken(SumDaily, 'timescaledb'));
const sumDailyRepository = module.get(getRepositoryToken(SumMarketplaceDaily, 'timescaledb'));
sumDailyRepository.createQueryBuilder = jest.fn(() => ({
select,
offset,
Expand All @@ -190,7 +195,7 @@ describe('Analytics Data Getter Service', () => {
orderBy,
groupBy,
}));
const result = await service.getVolumeData({
const result = await service.getVolumeDataWithMarketplaces({
time: '10d',
metric: 'test',
series: 'test',
Expand All @@ -209,7 +214,7 @@ describe('Analytics Data Getter Service', () => {
new AnalyticsAggregateValue({ value: 2, series: 'test' }),
new AnalyticsAggregateValue({ value: 121, series: 'test' }),
];
const sumDailyRepository = module.get(getRepositoryToken(SumDaily, 'timescaledb'));
const sumDailyRepository = module.get(getRepositoryToken(SumMarketplaceDaily, 'timescaledb'));
sumDailyRepository.createQueryBuilder = jest.fn(() => ({
select,
offset,
Expand All @@ -222,7 +227,7 @@ describe('Analytics Data Getter Service', () => {
groupBy,
}));

const response = await service.getVolumeData({
const response = await service.getVolumeDataWithMarketplaces({
time: '10d',
metric: 'test',
series: 'test',
Expand Down
4 changes: 2 additions & 2 deletions src/common/persistence/timescaledb/timescaledb.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ApiConfigService } from 'src/modules/common/api-config/api.config.servi
import { AnalyticsDataGetterService } from './analytics-data.getter.service';
import { AnalyticsDataSetterService } from './analytics-data.setter.service';
import { XNftsAnalyticsEntity } from './entities/analytics.entity';
import { FloorPriceDaily, SumDaily } from './entities/sum-daily.entity';
import { FloorPriceDaily, SumDaily, SumMarketplaceDaily } from './entities/sum-daily.entity';
import { SumWeekly } from './entities/sum-weekly.entity';

@Module({
Expand Down Expand Up @@ -34,7 +34,7 @@ import { SumWeekly } from './entities/sum-weekly.entity';
}),
inject: [ApiConfigService],
}),
TypeOrmModule.forFeature([XNftsAnalyticsEntity, SumDaily, SumWeekly, FloorPriceDaily], 'timescaledb'),
TypeOrmModule.forFeature([XNftsAnalyticsEntity, SumDaily, SumMarketplaceDaily, SumWeekly, FloorPriceDaily], 'timescaledb'),
],
providers: [AnalyticsDataGetterService, AnalyticsDataSetterService],
exports: [AnalyticsDataGetterService, AnalyticsDataSetterService],
Expand Down
2 changes: 1 addition & 1 deletion src/modules/analytics/analytics.getter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class AnalyticsGetterService {
return await this.cacheService.getOrSet(
cacheKey,
() =>
this.analyticsQuery.getVolumeData({
this.analyticsQuery.getVolumeDataWithMarketplaces({
series,
metric,
time,
Expand Down
49 changes: 47 additions & 2 deletions src/modules/analytics/models/analytics-aggregate-value.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Field, Float, ObjectType } from '@nestjs/graphql';
import { Field, Float, Int, ObjectType } from '@nestjs/graphql';
import * as moment from 'moment';

@ObjectType()
Expand All @@ -23,6 +23,8 @@ export class AnalyticsAggregateValue {

@Field(() => Float, { nullable: true })
avg?: number;
@Field(() => [MarketplaceData], { nullable: 'itemsAndList' })
marketplacesData: MarketplaceData[];

constructor(init?: Partial<AnalyticsAggregateValue>) {
Object.assign(this, init);
Expand All @@ -39,12 +41,55 @@ export class AnalyticsAggregateValue {
avg: row.avg ?? 0,
});
}
static fromTimescaleObject(row: any) {
return new AnalyticsAggregateValue({
series: row.series,
time: moment.utc(row.timestamp ?? row.time).format('yyyy-MM-DD HH:mm:ss'),
value: row.value ?? 0,
});
}

static fromTimescaleObjectWithMarketplaces(row: any) {
const rowProperties = proxiedPropertiesOf(row);

static fromTimescaleObjext(row: any) {
return new AnalyticsAggregateValue({
series: row.series,
time: moment.utc(row.timestamp ?? row.time).format('yyyy-MM-DD HH:mm:ss'),
value: row.value ?? 0,
marketplacesData: [
new MarketplaceData({ key: rowProperties.xoxno, value: row.xoxno ?? 0 }),
new MarketplaceData({ key: rowProperties.frameit, value: row.frameit ?? 0 }),
new MarketplaceData({ key: rowProperties.deadrare, value: row.deadrare ?? 0 }),
new MarketplaceData({ key: rowProperties.elrondapes, value: row.elrondapes ?? 0 }),
new MarketplaceData({ key: rowProperties.elrondnftswap, value: row.elrondnftswap ?? 0 }),
new MarketplaceData({ key: rowProperties.eneftor, value: row.eneftor ?? 0 }),
new MarketplaceData({ key: rowProperties.hoghomies, value: row.hoghomies ?? 0 }),
new MarketplaceData({ key: rowProperties.holoride, value: row.holoride ?? 0 }),
new MarketplaceData({ key: rowProperties.aquaverse, value: row.aquaverse ?? 0 }),
new MarketplaceData({ key: rowProperties.ici, value: row.ici ?? 0 }),
],
});
}
}

@ObjectType()
export class MarketplaceData {
@Field()
key: String;
@Field(() => Float, { nullable: true })
value: number;
constructor(init?: Partial<MarketplaceData>) {
Object.assign(this, init);
}
}

export function proxiedPropertiesOf<TObj>(obj?: TObj) {
return new Proxy(
{},
{
get: (_, prop) => prop,
},
) as {
[P in keyof TObj]?: P;
};
}

0 comments on commit f3266c1

Please sign in to comment.