Skip to content

Commit

Permalink
fix: etag cache calculation with url prefix (#111)
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelcr committed Feb 9, 2023
1 parent d0935b9 commit f872f93
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 4 deletions.
17 changes: 14 additions & 3 deletions src/api/util/cache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FastifyReply, FastifyRequest } from 'fastify';
import { logger } from '../../logger';
import { SmartContractRegEx } from '../types';

/**
* A `Cache-Control` header used for re-validation based caching.
Expand Down Expand Up @@ -28,9 +29,19 @@ export async function handleTokenCache(request: FastifyRequest, reply: FastifyRe
async function getTokenEtag(request: FastifyRequest): Promise<string | undefined> {
try {
const components = request.url.split('/');
components.shift();
const contractPrincipal = components[1];
const tokenNumber = components[0] === 'ft' ? 1 : Number(components[2]);
let tokenNumber: bigint = 1n;
let contractPrincipal: string | undefined;
do {
const lastElement = components.pop();
if (lastElement && lastElement.length) {
if (SmartContractRegEx.test(lastElement)) {
contractPrincipal = lastElement;
} else if (/^\d+$/.test(lastElement)) {
tokenNumber = BigInt(lastElement);
}
}
} while (components.length);
if (!contractPrincipal) return;
return await request.server.db.getTokenEtag({ contractPrincipal, tokenNumber });
} catch (error) {
return undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/pg/pg-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ export class PgStore extends BasePgStore {
*/
async getTokenEtag(args: {
contractPrincipal: string;
tokenNumber: number;
tokenNumber: bigint;
}): Promise<string | undefined> {
const result = await this.sql<{ etag: string }[]>`
SELECT date_part('epoch', t.updated_at)::text AS etag
Expand Down
233 changes: 233 additions & 0 deletions tests/cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import { ENV } from '../src/env';
import { cycleMigrations } from '../src/pg/migrations';
import { PgStore } from '../src/pg/pg-store';
import { DbSmartContractInsert, DbSipNumber, DbTokenType } from '../src/pg/types';
import { TestFastifyServer, startTestApiServer } from './helpers';

describe('ETag cache', () => {
let db: PgStore;
let fastify: TestFastifyServer;

beforeEach(async () => {
ENV.PGDATABASE = 'postgres';
db = await PgStore.connect({ skipMigrations: true });
fastify = await startTestApiServer(db);
await cycleMigrations();
});

afterEach(async () => {
await fastify.close();
await db.close();
});

test('FT cache control', async () => {
const values: DbSmartContractInsert = {
principal: 'SP2SYHR84SDJJDK8M09HFS4KBFXPPCX9H7RZ9YVTS.hello-world',
sip: DbSipNumber.sip010,
abi: '"some"',
tx_id: '0x123456',
block_height: 1,
};
await db.insertAndEnqueueSmartContract({ values });
await db.insertAndEnqueueSequentialTokens({
smart_contract_id: 1,
token_count: 1n,
type: DbTokenType.ft,
});
await db.updateProcessedTokenWithMetadata({
id: 1,
values: {
token: {
name: 'hello-world',
symbol: 'HELLO',
decimals: 6,
total_supply: '1',
uri: 'http://test.com/uri.json',
},
metadataLocales: [
{
metadata: {
sip: 16,
token_id: 1,
name: 'hello-world',
l10n_locale: 'en',
l10n_uri: null,
l10n_default: true,
description: 'test',
image: 'http://test.com/image.png',
cached_image: 'http://test.com/image.png?processed=true',
},
},
],
},
});

// Request returns etag
const response = await fastify.inject({
method: 'GET',
url: '/metadata/v1/ft/SP2SYHR84SDJJDK8M09HFS4KBFXPPCX9H7RZ9YVTS.hello-world',
});
expect(response.statusCode).toBe(200);
expect(response.headers.etag).not.toBeUndefined();
const etag = response.headers.etag;

// Cached response
const cached = await fastify.inject({
method: 'GET',
url: '/metadata/v1/ft/SP2SYHR84SDJJDK8M09HFS4KBFXPPCX9H7RZ9YVTS.hello-world',
headers: { 'if-none-match': etag },
});
expect(cached.statusCode).toBe(304);

// Simulate modified token and check status code
await db.sql`UPDATE tokens SET updated_at = NOW() WHERE id = 1`;
const cached2 = await fastify.inject({
method: 'GET',
url: '/metadata/v1/ft/SP2SYHR84SDJJDK8M09HFS4KBFXPPCX9H7RZ9YVTS.hello-world',
headers: { 'if-none-match': etag },
});
expect(cached2.statusCode).toBe(200);
});

test('NFT cache control', async () => {
const values: DbSmartContractInsert = {
principal: 'SP2SYHR84SDJJDK8M09HFS4KBFXPPCX9H7RZ9YVTS.hello-world',
sip: DbSipNumber.sip009,
abi: '"some"',
tx_id: '0x123456',
block_height: 1,
};
await db.insertAndEnqueueSmartContract({ values });
await db.insertAndEnqueueSequentialTokens({
smart_contract_id: 1,
token_count: 1n,
type: DbTokenType.nft,
});
await db.updateProcessedTokenWithMetadata({
id: 1,
values: {
token: {
name: 'hello-world',
symbol: null,
decimals: null,
total_supply: '1',
uri: 'http://test.com/uri.json',
},
metadataLocales: [
{
metadata: {
sip: 16,
token_id: 1,
name: 'hello-world',
l10n_locale: 'en',
l10n_uri: null,
l10n_default: true,
description: 'test',
image: null,
cached_image: null,
},
},
],
},
});

// Request returns etag
const response = await fastify.inject({
method: 'GET',
url: '/metadata/v1/nft/SP2SYHR84SDJJDK8M09HFS4KBFXPPCX9H7RZ9YVTS.hello-world/1',
});
expect(response.statusCode).toBe(200);
expect(response.headers.etag).not.toBeUndefined();
const etag = response.headers.etag;

// Cached response
const cached = await fastify.inject({
method: 'GET',
url: '/metadata/v1/nft/SP2SYHR84SDJJDK8M09HFS4KBFXPPCX9H7RZ9YVTS.hello-world/1',
headers: { 'if-none-match': etag },
});
expect(cached.statusCode).toBe(304);

// Simulate modified token and check status code
await db.sql`UPDATE tokens SET updated_at = NOW() WHERE id = 1`;
const cached2 = await fastify.inject({
method: 'GET',
url: '/metadata/v1/nft/SP2SYHR84SDJJDK8M09HFS4KBFXPPCX9H7RZ9YVTS.hello-world/1',
headers: { 'if-none-match': etag },
});
expect(cached2.statusCode).toBe(200);
});

test('SFT cache control', async () => {
const address = 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9';
const contractId = 'key-alex-autoalex-v1';
const values: DbSmartContractInsert = {
principal: `${address}.${contractId}`,
sip: DbSipNumber.sip013,
abi: '"some"',
tx_id: '0x123456',
block_height: 1,
};
await db.insertAndEnqueueSmartContract({ values });
await db.insertAndEnqueueTokenArray([
{
smart_contract_id: 1,
type: DbTokenType.sft,
token_number: '1',
},
]);
await db.updateProcessedTokenWithMetadata({
id: 1,
values: {
token: {
name: null,
symbol: null,
decimals: 6,
total_supply: '200',
uri: 'http://test.com/uri.json',
},
metadataLocales: [
{
metadata: {
sip: 16,
token_id: 1,
name: 'key-alex-autoalex-v1',
l10n_locale: 'en',
l10n_uri: null,
l10n_default: true,
description: 'test',
image: 'http://test.com/image.png',
cached_image: 'http://test.com/image.png?processed=true',
},
},
],
},
});

// Request returns etag
const response = await fastify.inject({
method: 'GET',
url: '/metadata/v1/sft/SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.key-alex-autoalex-v1/1',
});
expect(response.statusCode).toBe(200);
expect(response.headers.etag).not.toBeUndefined();
const etag = response.headers.etag;

// Cached response
const cached = await fastify.inject({
method: 'GET',
url: '/metadata/v1/sft/SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.key-alex-autoalex-v1/1',
headers: { 'if-none-match': etag },
});
expect(cached.statusCode).toBe(304);

// Simulate modified token and check status code
await db.sql`UPDATE tokens SET updated_at = NOW() WHERE id = 1`;
const cached2 = await fastify.inject({
method: 'GET',
url: '/metadata/v1/sft/SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.key-alex-autoalex-v1/1',
headers: { 'if-none-match': etag },
});
expect(cached2.statusCode).toBe(200);
});
});

0 comments on commit f872f93

Please sign in to comment.