From 12038fc687276c4062da851d08405cadc1fcc944 Mon Sep 17 00:00:00 2001 From: bitbeckers Date: Tue, 11 Feb 2025 11:41:02 +0100 Subject: [PATCH 1/2] feat(service): introduce image fetching service Refactors the SupabaseCachingService to get all hypercert columns except for the image. Renamed the getMetadata method to getMetadataWithoutImage to make this explicit. Introduces MetadataImageService for fetching the image when the field is selected in a metadata query. --- src/graphql/schemas/resolvers/baseTypes.ts | 5 ++- .../schemas/resolvers/fractionResolver.ts | 2 +- .../schemas/resolvers/hyperboardResolver.ts | 2 +- .../schemas/resolvers/hypercertResolver.ts | 2 +- .../schemas/resolvers/metadataResolver.ts | 29 +++++++++++++- .../schemas/resolvers/orderResolver.ts | 2 +- .../schemas/resolvers/salesResolver.ts | 2 +- .../schemas/typeDefs/metadataTypeDefs.ts | 5 --- src/services/MetadataImageService.ts | 30 ++++++++++++++ src/services/SupabaseCachingService.ts | 39 ++++++++++++++++++- 10 files changed, 102 insertions(+), 16 deletions(-) create mode 100644 src/services/MetadataImageService.ts diff --git a/src/graphql/schemas/resolvers/baseTypes.ts b/src/graphql/schemas/resolvers/baseTypes.ts index 1bafa13b..e02e7d8d 100644 --- a/src/graphql/schemas/resolvers/baseTypes.ts +++ b/src/graphql/schemas/resolvers/baseTypes.ts @@ -38,13 +38,14 @@ export function createBaseResolver( readonly supabaseCachingService = container.resolve(SupabaseCachingService); readonly supabaseDataService = container.resolve(SupabaseDataService); - getMetadata(args: GetMetadataArgs, single: boolean = false) { + getMetadataWithoutImage(args: GetMetadataArgs, single: boolean = false) { console.debug( `[${entityFieldName}Resolver::getMetadata] Fetching metadata`, ); try { - const queries = this.supabaseCachingService.getMetadata(args); + const queries = + this.supabaseCachingService.getMetadataWithoutImage(args); if (single) { return queries.data.executeTakeFirst(); } diff --git a/src/graphql/schemas/resolvers/fractionResolver.ts b/src/graphql/schemas/resolvers/fractionResolver.ts index d3653c1b..b2c1cf02 100644 --- a/src/graphql/schemas/resolvers/fractionResolver.ts +++ b/src/graphql/schemas/resolvers/fractionResolver.ts @@ -29,7 +29,7 @@ class FractionResolver extends FractionBaseResolver { return; } - return await this.getMetadata( + return await this.getMetadataWithoutImage( { where: { hypercerts: { id: { eq: fraction.claims_id } } }, }, diff --git a/src/graphql/schemas/resolvers/hyperboardResolver.ts b/src/graphql/schemas/resolvers/hyperboardResolver.ts index 1ed40869..b29dc858 100644 --- a/src/graphql/schemas/resolvers/hyperboardResolver.ts +++ b/src/graphql/schemas/resolvers/hyperboardResolver.ts @@ -42,7 +42,7 @@ class HyperboardResolver extends HyperboardBaseResolver { }).then((res) => res.data), ]); - const metadata = await this.getMetadata({ + const metadata = await this.getMetadataWithoutImage({ where: { hypercerts: { hypercert_id: { in: hypercertIds } } }, }) .then((res) => res.data) diff --git a/src/graphql/schemas/resolvers/hypercertResolver.ts b/src/graphql/schemas/resolvers/hypercertResolver.ts index a3df1a68..a97ed4c3 100644 --- a/src/graphql/schemas/resolvers/hypercertResolver.ts +++ b/src/graphql/schemas/resolvers/hypercertResolver.ts @@ -38,7 +38,7 @@ class HypercertResolver extends HypercertBaseResolver { return; } - return await this.getMetadata( + return await this.getMetadataWithoutImage( { where: { uri: { eq: hypercert.uri } } }, true, ); diff --git a/src/graphql/schemas/resolvers/metadataResolver.ts b/src/graphql/schemas/resolvers/metadataResolver.ts index 20885b02..1f56ded1 100644 --- a/src/graphql/schemas/resolvers/metadataResolver.ts +++ b/src/graphql/schemas/resolvers/metadataResolver.ts @@ -1,18 +1,43 @@ -import { Args, ObjectType, Query, Resolver } from "type-graphql"; +import { + Args, + FieldResolver, + ObjectType, + Query, + Resolver, + Root, +} from "type-graphql"; +import { inject, singleton } from "tsyringe"; import { Metadata } from "../typeDefs/metadataTypeDefs.js"; import { GetMetadataArgs } from "../args/metadataArgs.js"; import { createBaseResolver, DataResponse } from "./baseTypes.js"; +import { MetadataImageService } from "../../../services/MetadataImageService.js"; @ObjectType() export class GetMetadataResponse extends DataResponse(Metadata) {} const MetadataBaseResolver = createBaseResolver("metadata"); +@singleton() @Resolver(() => Metadata) class MetadataResolver extends MetadataBaseResolver { + constructor( + @inject(MetadataImageService) private imageService: MetadataImageService, + ) { + super(); + } + @Query(() => GetMetadataResponse) async metadata(@Args() args: GetMetadataArgs) { - return await this.getMetadata(args); + return await this.getMetadataWithoutImage(args); + } + + @FieldResolver(() => String, { + nullable: true, + description: "Base64 encoded representation of the image of the hypercert", + }) + async image(@Root() metadata: Metadata) { + if (!metadata.uri) return null; + return await this.imageService.getImageByUri(metadata.uri); } } diff --git a/src/graphql/schemas/resolvers/orderResolver.ts b/src/graphql/schemas/resolvers/orderResolver.ts index e780ac05..0e7947b4 100644 --- a/src/graphql/schemas/resolvers/orderResolver.ts +++ b/src/graphql/schemas/resolvers/orderResolver.ts @@ -107,7 +107,7 @@ class OrderResolver extends OrderBaseResolver { true, ); - const metadata = await this.getMetadata( + const metadata = await this.getMetadataWithoutImage( { where: { hypercerts: { diff --git a/src/graphql/schemas/resolvers/salesResolver.ts b/src/graphql/schemas/resolvers/salesResolver.ts index 76fadd37..760079c5 100644 --- a/src/graphql/schemas/resolvers/salesResolver.ts +++ b/src/graphql/schemas/resolvers/salesResolver.ts @@ -48,7 +48,7 @@ class SalesResolver extends SalesBaseResolver { return null; } - const metadata = await this.getMetadata( + const metadata = await this.getMetadataWithoutImage( { where: { hypercerts: { diff --git a/src/graphql/schemas/typeDefs/metadataTypeDefs.ts b/src/graphql/schemas/typeDefs/metadataTypeDefs.ts index 71a40d78..99c325eb 100644 --- a/src/graphql/schemas/typeDefs/metadataTypeDefs.ts +++ b/src/graphql/schemas/typeDefs/metadataTypeDefs.ts @@ -14,11 +14,6 @@ class Metadata extends BasicTypeDef { name?: string; @Field({ nullable: true, description: "Description of the hypercert" }) description?: string; - @Field({ - nullable: true, - description: "Base64 encoded representation of the image of the hypercert", - }) - image?: string; @Field({ nullable: true, description: "URI of the hypercert metadata" }) uri?: string; @Field({ diff --git a/src/services/MetadataImageService.ts b/src/services/MetadataImageService.ts new file mode 100644 index 00000000..7f14e694 --- /dev/null +++ b/src/services/MetadataImageService.ts @@ -0,0 +1,30 @@ +import { singleton } from "tsyringe"; +import { kyselyCaching } from "../client/kysely.js"; +import { CachingDatabase } from "../types/kyselySupabaseCaching.js"; +import { BaseSupabaseService } from "./BaseSupabaseService.js"; + +@singleton() +export class MetadataImageService extends BaseSupabaseService { + constructor() { + super(kyselyCaching); + } + + // TODO: remove these when we more refactor the services to improve typing and performance + getDataQuery() { + throw new Error("Method not implemented - not needed for image service"); + } + + getCountQuery() { + throw new Error("Method not implemented - not needed for image service"); + } + + async getImageByUri(uri: string): Promise { + const result = await this.db + .selectFrom("metadata") + .select(["image"]) + .where("uri", "=", uri) + .executeTakeFirst(); + + return result?.image ?? null; + } +} diff --git a/src/services/SupabaseCachingService.ts b/src/services/SupabaseCachingService.ts index 89670b56..f12e238f 100644 --- a/src/services/SupabaseCachingService.ts +++ b/src/services/SupabaseCachingService.ts @@ -56,7 +56,7 @@ export class SupabaseCachingService extends BaseSupabaseService }; } - getMetadata(args: GetMetadataArgs) { + getMetadataWithoutImage(args: GetMetadataArgs) { return { data: this.handleGetData("metadata", args), count: this.handleGetCount("metadata", args), @@ -138,9 +138,44 @@ export class SupabaseCachingService extends BaseSupabaseService case "fractions_view": return this.db.selectFrom("fractions_view").selectAll(); case "metadata": + // Skip the image column + // 1. id + // 2. name + // 3. description + // 4. image + // 5. external_url + // 6. work_scope + // 7. work_timeframe_from + // 8. work_timeframe_to + // 9. impact_scope + // 10. impact_timeframe_from + // 11. impact_timeframe_to + // 12. contributors + // 13. rights + // 14. uri + // 15. properties + // 16. allow_list_uri + // 17. parsed return this.db .selectFrom("metadata") - .selectAll("metadata") + .select([ + "id", + "name", + "description", + "external_url", + "work_scope", + "work_timeframe_from", + "work_timeframe_to", + "impact_scope", + "impact_timeframe_from", + "impact_timeframe_to", + "contributors", + "rights", + "uri", + "properties", + "allow_list_uri", + "parsed", + ]) .$if(args.where?.hypercerts, (qb) => qb.innerJoin("claims", "claims.uri", "metadata.uri"), ); From 43eb06bb16774d342bc173e59954324ecae0f2c4 Mon Sep 17 00:00:00 2001 From: bitbeckers Date: Tue, 11 Feb 2025 12:58:57 +0100 Subject: [PATCH 2/2] feat(compression): compressed metadata images When the MetadataImageService gets a request, try and fetch the image form an in memory cache. If not present, fetch from Supabase and compress the image using sharp. Opted for compressions with webp as that should be nice for web images. Proof of concept --- package.json | 2 + pnpm-lock.yaml | 301 ++++++++++++++++++++++++++- src/services/MetadataImageService.ts | 41 +++- 3 files changed, 333 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index f4e2014a..2a1c8139 100644 --- a/package.json +++ b/package.json @@ -67,10 +67,12 @@ "lru-cache": "^11.0.0", "mime-types": "^2.1.35", "multer": "1.4.5-lts.1", + "node-cache": "^5.1.2", "node-cron": "^3.0.3", "pg": "^8.12.0", "reflect-metadata": "^0.2.2", "rollup": "^4.12.0", + "sharp": "^0.33.5", "swagger-ui-express": "^5.0.0", "tsoa": "^6.2.1", "tsyringe": "^4.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2da93c03..08e7eedb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,6 +131,9 @@ importers: multer: specifier: 1.4.5-lts.1 version: 1.4.5-lts.1 + node-cache: + specifier: ^5.1.2 + version: 5.1.2 node-cron: specifier: ^3.0.3 version: 3.0.3 @@ -143,6 +146,9 @@ importers: rollup: specifier: ^4.12.0 version: 4.12.0 + sharp: + specifier: ^0.33.5 + version: 0.33.5 swagger-ui-express: specifier: ^5.0.0 version: 5.0.0(express@4.19.2) @@ -360,6 +366,7 @@ packages: '@ardatan/relay-compiler@12.0.0': resolution: {integrity: sha512-9anThAaj1dQr6IGmzBMcfzOQKTa5artjuPmw8NYK/fiGEMjADbSguBY2FMDykt+QhilR3wc9VA/3yVju7JHg7Q==} + hasBin: true peerDependencies: graphql: '*' @@ -773,6 +780,9 @@ packages: peerDependencies: graphql: ^15.0.0 || ^16.0.0 + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@envelop/core@5.0.0': resolution: {integrity: sha512-aJdnH/ptv+cvwfvciCBe7TSvccBwo9g0S5f6u35TBVzRVqIGkK03lFlIL+x1cnfZgN9EfR2b1PH2galrT1CdCQ==} engines: {node: '>=18.0.0'} @@ -1083,6 +1093,7 @@ packages: '@graphql-codegen/cli@5.0.2': resolution: {integrity: sha512-MBIaFqDiLKuO4ojN6xxG9/xL9wmfD3ZjZ7RsPjwQnSHBCUXnEkdKvX+JVpx87Pq29Ycn8wTJUguXnTZ7Di0Mlw==} + hasBin: true peerDependencies: '@parcel/watcher': ^2.1.0 graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 @@ -1507,6 +1518,111 @@ packages: '@hypercerts-org/sdk@2.5.0-beta.6': resolution: {integrity: sha512-v24hjmCwkL2/lkbQbYxzepLAJOc2SwfHVBoADNcdcT+/s7Fvpq5I+MddlWHYDcBLacPhyF3k+F9O/tkwvofY1g==} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@inquirer/checkbox@2.3.5': resolution: {integrity: sha512-3V0OSykTkE/38GG1DhxRGLBmqefgzRg2EK5A375zz+XEvIWfAHcac31e+zlBDPypRHxhmXc/Oh6v9eOPbH3nAg==} engines: {node: '>=18'} @@ -2152,6 +2268,7 @@ packages: '@openzeppelin/hardhat-upgrades@3.2.1': resolution: {integrity: sha512-Zy5M3QhkzwGdpzQmk+xbWdYOGJWjoTvwbBKYLhctu9B91DoprlhDRaZUwCtunwTdynkTDGdVfGr0kIkvycyKjw==} + hasBin: true peerDependencies: '@nomicfoundation/hardhat-ethers': ^3.0.0 '@nomicfoundation/hardhat-verify': ^2.0.0 @@ -2516,6 +2633,7 @@ packages: '@snaplet/seed@0.97.20': resolution: {integrity: sha512-+lnqESgwP92O1266vsTyoRgrg4hMCUTybBUxDT1ICMBFcvdjgwcOaUt8Xjj81YvxYkZlu5+TTBIjyNQT4nP4jQ==} engines: {node: '>=18.5.0'} + hasBin: true peerDependencies: '@prisma/client': '>=5' '@snaplet/copycat': '>=2' @@ -2574,6 +2692,7 @@ packages: '@swc/cli@0.3.12': resolution: {integrity: sha512-h7bvxT+4+UDrLWJLFHt6V+vNAcUNii2G4aGSSotKz1ECEk4MyEh5CWxmeSscwuz5K3i+4DWTgm4+4EyMCQKn+g==} engines: {node: '>= 16.14.0'} + hasBin: true peerDependencies: '@swc/core': ^1.2.66 chokidar: ^3.5.1 @@ -3873,6 +3992,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + cmd-shim@6.0.3: resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -3899,6 +4022,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -4875,6 +5005,7 @@ packages: gql.tada@1.8.3: resolution: {integrity: sha512-0H81I3M54jKTDHbnNWhXDf57Ie2d2raxnFCc93zdYjXHnrXe522jrio9AAFwqBlGx/xtaP3ILSSUw7J9H31LAA==} + hasBin: true peerDependencies: typescript: ^5.0.0 @@ -4956,6 +5087,7 @@ packages: hardhat@2.22.18: resolution: {integrity: sha512-2+kUz39gvMo56s75cfLBhiFedkQf+gXdrwCcz4R/5wW0oBdwiyfj2q9BIkMoaA0WIGYYMU2I1Cc4ucTunhfjzw==} + hasBin: true peerDependencies: ts-node: '*' typescript: '*' @@ -5167,6 +5299,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-bigint@1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} @@ -6014,6 +6149,10 @@ packages: node-addon-api@3.2.1: resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} + node-cache@5.1.2: + resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==} + engines: {node: '>= 8.0.0'} + node-cron@3.0.3: resolution: {integrity: sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==} engines: {node: '>=6.0.0'} @@ -6803,6 +6942,7 @@ packages: semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} @@ -6853,6 +6993,10 @@ packages: sha.js@2.4.11: resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -6894,6 +7038,9 @@ packages: signedsource@1.0.0: resolution: {integrity: sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + simple-update-notifier@2.0.0: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} @@ -7280,6 +7427,7 @@ packages: ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true peerDependencies: '@swc/core': '>=1.2.50' '@swc/wasm': '>=1.2.50' @@ -7294,6 +7442,7 @@ packages: tsconfck@3.1.4: resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==} engines: {node: ^18 || >=20} + hasBin: true peerDependencies: typescript: ^5.0.0 peerDependenciesMeta: @@ -7407,6 +7556,7 @@ packages: typedoc@0.26.5: resolution: {integrity: sha512-Vn9YKdjKtDZqSk+by7beZ+xzkkr8T8CYoiasqyt4TTRFy5+UHzL/mF/o4wGBjRF+rlWQHDb0t6xCpA3JNL5phg==} engines: {node: '>= 18'} + hasBin: true peerDependencies: typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x @@ -7509,6 +7659,7 @@ packages: update-browserslist-db@1.0.13: resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -7613,6 +7764,7 @@ packages: vite@5.0.11: resolution: {integrity: sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==} engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true peerDependencies: '@types/node': ^18.0.0 || >=20.0.0 less: '*' @@ -7646,6 +7798,7 @@ packages: vitest@2.1.8: resolution: {integrity: sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==} engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 @@ -8420,7 +8573,7 @@ snapshots: '@commitlint/is-ignored@19.2.2': dependencies: '@commitlint/types': 19.0.3 - semver: 7.6.0 + semver: 7.6.3 '@commitlint/lint@19.4.1': dependencies: @@ -8516,6 +8669,11 @@ snapshots: - encoding - supports-color + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.6.2 + optional: true + '@envelop/core@5.0.0': dependencies: '@envelop/types': 5.0.0 @@ -9799,6 +9957,81 @@ snapshots: - typescript - utf-8-validate + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@inquirer/checkbox@2.3.5': dependencies: '@inquirer/core': 8.2.2 @@ -10398,7 +10631,7 @@ snapshots: '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 - semver: 7.6.0 + semver: 7.6.3 transitivePeerDependencies: - supports-color @@ -10483,7 +10716,7 @@ snapshots: '@types/shimmer': 1.0.5 import-in-the-middle: 1.4.2 require-in-the-middle: 7.3.0 - semver: 7.6.0 + semver: 7.6.3 shimmer: 1.2.1 transitivePeerDependencies: - supports-color @@ -10496,7 +10729,7 @@ snapshots: '@types/shimmer': 1.0.5 import-in-the-middle: 1.7.1 require-in-the-middle: 7.3.0 - semver: 7.6.0 + semver: 7.6.3 shimmer: 1.2.1 transitivePeerDependencies: - supports-color @@ -10508,7 +10741,7 @@ snapshots: '@types/shimmer': 1.0.5 import-in-the-middle: 1.7.4 require-in-the-middle: 7.3.0 - semver: 7.6.0 + semver: 7.6.3 shimmer: 1.2.1 transitivePeerDependencies: - supports-color @@ -11707,7 +11940,7 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.4 - semver: 7.6.0 + semver: 7.6.3 ts-api-utils: 1.3.0(typescript@5.5.3) optionalDependencies: typescript: 5.5.3 @@ -12404,7 +12637,7 @@ snapshots: bin-version-check@5.1.0: dependencies: bin-version: 6.0.0 - semver: 7.5.4 + semver: 7.6.3 semver-truncate: 3.0.0 bin-version@6.0.0: @@ -12804,6 +13037,8 @@ snapshots: clone@1.0.4: {} + clone@2.1.2: {} + cmd-shim@6.0.3: {} code-block-writer@12.0.0: {} @@ -12830,6 +13065,16 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + colorette@2.0.20: {} combined-stream@1.0.8: @@ -12887,7 +13132,7 @@ snapshots: dot-prop: 7.2.0 env-paths: 3.0.0 json-schema-typed: 8.0.1 - semver: 7.6.0 + semver: 7.6.3 confbox@0.1.7: {} @@ -14472,6 +14717,8 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.2: {} + is-bigint@1.0.4: dependencies: has-bigints: 1.0.2 @@ -15277,13 +15524,17 @@ snapshots: node-abi@3.62.0: dependencies: - semver: 7.6.0 + semver: 7.6.3 node-addon-api@2.0.2: {} node-addon-api@3.2.1: optional: true + node-cache@5.1.2: + dependencies: + clone: 2.1.2 + node-cron@3.0.3: dependencies: uuid: 8.3.2 @@ -16137,7 +16388,7 @@ snapshots: semver-truncate@3.0.0: dependencies: - semver: 7.6.0 + semver: 7.6.3 semver@5.7.2: {} @@ -16217,6 +16468,32 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + shebang-command@1.2.0: dependencies: shebang-regex: 1.0.0 @@ -16252,6 +16529,10 @@ snapshots: signedsource@1.0.0: {} + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + simple-update-notifier@2.0.0: dependencies: semver: 7.5.4 diff --git a/src/services/MetadataImageService.ts b/src/services/MetadataImageService.ts index 7f14e694..1d2c7315 100644 --- a/src/services/MetadataImageService.ts +++ b/src/services/MetadataImageService.ts @@ -2,11 +2,17 @@ import { singleton } from "tsyringe"; import { kyselyCaching } from "../client/kysely.js"; import { CachingDatabase } from "../types/kyselySupabaseCaching.js"; import { BaseSupabaseService } from "./BaseSupabaseService.js"; +import NodeCache from "node-cache"; +import sharp from "sharp"; @singleton() export class MetadataImageService extends BaseSupabaseService { + private cache: NodeCache; + private readonly CACHE_TTL = 60 * 60 * 24; // 24 hours + constructor() { super(kyselyCaching); + this.cache = new NodeCache({ stdTTL: this.CACHE_TTL }); } // TODO: remove these when we more refactor the services to improve typing and performance @@ -19,12 +25,45 @@ export class MetadataImageService extends BaseSupabaseService { } async getImageByUri(uri: string): Promise { + // Check cache first + const cachedImage = this.cache.get(uri); + if (cachedImage) return cachedImage; + + // Fetch from database if not cached const result = await this.db .selectFrom("metadata") .select(["image"]) .where("uri", "=", uri) .executeTakeFirst(); - return result?.image ?? null; + if (!result?.image) return null; + + console.log("result", result.image.slice(0, 100)); + + // Compress image + const compressedImage = await this.compressImage(result.image); + + // Cache the compressed result + this.cache.set(uri, compressedImage); + + return compressedImage; + } + + private async compressImage(base64Image: string): Promise { + // TODO: if image is an URL because of 3rd party input, we should fetch and compress the image + + // Remove the data URL prefix if present + const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, ""); + + const imageBuffer = Buffer.from(base64Data, "base64"); + const compressedBuffer = await sharp(imageBuffer) + .webp({ + quality: 80, + effort: 4, + }) + .toBuffer(); + + // Add back the appropriate data URL prefix for WebP + return `data:image/webp;base64,${compressedBuffer.toString("base64")}`; } }