From 01c05e225f01b22093ddaf0086680b95f15c361c Mon Sep 17 00:00:00 2001 From: Matthew Bunday Date: Thu, 27 Jun 2024 17:50:36 -0400 Subject: [PATCH] Feat: OCS registry api (#539) * Attempt DB query * Super basic demo * Clean up * Remove content nesting * Fix lint * Update for API compatibility * Add pagination to entries api * Add featured API and entries pagination * Add category filtering * Pass through formatted data, no need to transform * Make category filtering case insensitive * add curation filter * Move ocsRegistry -> /api/registry --------- Co-authored-by: Ricardo Moguel --- apps/web/package.json | 2 + apps/web/pages/api/registry/entries.ts | 46 ++++++++ apps/web/pages/api/registry/featured.ts | 19 ++++ apps/web/src/utils/ocsRegistry.ts | 31 ++++++ yarn.lock | 134 +++++++++++++++++++++++- 5 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 apps/web/pages/api/registry/entries.ts create mode 100644 apps/web/pages/api/registry/featured.ts create mode 100644 apps/web/src/utils/ocsRegistry.ts diff --git a/apps/web/package.json b/apps/web/package.json index f2d3d02125..0fcd85850c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -23,6 +23,7 @@ "framer-motion": "^8.5.5", "kysely": "^0.27.3", "next": "^13.2.0", + "pg": "^8.12.0", "react": "^18.2.0", "react-blockies": "^1.4.1", "react-copy-to-clipboard": "^5.1.0", @@ -37,6 +38,7 @@ }, "devDependencies": { "@types/node": "18.11.18", + "@types/pg": "^8.11.6", "@types/react-copy-to-clipboard": "^5.0.7", "autoprefixer": "^10.4.13", "csv-parser": "^3.0.0", diff --git a/apps/web/pages/api/registry/entries.ts b/apps/web/pages/api/registry/entries.ts new file mode 100644 index 0000000000..d3032a9ff0 --- /dev/null +++ b/apps/web/pages/api/registry/entries.ts @@ -0,0 +1,46 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { db } from 'apps/web/src/utils/ocsRegistry'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { page = '1', limit = '10', category, curation } = req.query; + + const pageNum = parseInt(page as string, 10); + const limitNum = parseInt(limit as string, 10); + const offset = (pageNum - 1) * limitNum; + + // Base query for filtering by category if provided + let baseQuery = db.selectFrom('content'); + + if (category) { + baseQuery = baseQuery.where('category', 'ilike', `%${category}%`); + } + + if (curation) { + baseQuery = baseQuery.where('curation', 'ilike', `%${curation}%`); + } + + // Fetch total records count + const totalRecordsQuery = baseQuery.select(db.fn.count('id').as('count')); + const totalRecords = await totalRecordsQuery.execute(); + const totalRecordsCount = parseInt(totalRecords[0].count as string, 10); + + // Fetch paginated content + const contentQuery = baseQuery.selectAll().limit(limitNum).offset(offset); + const content = await contentQuery.execute(); + + const response = { + data: content.map((row) => ({ + id: row.id, + category: row.category, + content: row.content, + })), + pagination: { + total_records: totalRecordsCount, + current_page: pageNum, + total_pages: Math.ceil(totalRecordsCount / limitNum), + limit: limitNum, + }, + }; + + res.status(200).json(response); +} diff --git a/apps/web/pages/api/registry/featured.ts b/apps/web/pages/api/registry/featured.ts new file mode 100644 index 0000000000..1921f80871 --- /dev/null +++ b/apps/web/pages/api/registry/featured.ts @@ -0,0 +1,19 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { db } from 'apps/web/src/utils/ocsRegistry'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const content = await db + .selectFrom('content') + .where('is_featured', '=', true) + .selectAll() + .limit(1) + .execute(); + + const row = content[0]; + + const response = { + data: row, + }; + + res.status(200).json(response); +} diff --git a/apps/web/src/utils/ocsRegistry.ts b/apps/web/src/utils/ocsRegistry.ts new file mode 100644 index 0000000000..9bbf102dc9 --- /dev/null +++ b/apps/web/src/utils/ocsRegistry.ts @@ -0,0 +1,31 @@ +import { ColumnType, JSONColumnType } from 'kysely'; +import { createKysely } from '@vercel/postgres-kysely'; + +type Database = { + content: ContentTable; +}; + +type OcsChallengeCard = { + title: string; + short_description: string; + full_description: string; + image_url: string; + target_url: string; + cta_text: string; + function_signature: string; + contract_address: string; + token_id: string; + token_amount: string; + creator_name: string; + creator_image_url: string; +}; + +type ContentTable = { + id: string; + category: string; + created_at: ColumnType; + is_featured: boolean; + content: JSONColumnType; +}; + +export const db = createKysely(); diff --git a/yarn.lock b/yarn.lock index fc516855f4..0558eee434 100644 --- a/yarn.lock +++ b/yarn.lock @@ -311,6 +311,7 @@ __metadata: "@rainbow-me/rainbowkit": ^2.1.2 "@tanstack/react-query": ^5.29.2 "@types/node": 18.11.18 + "@types/pg": ^8.11.6 "@types/react-copy-to-clipboard": ^5.0.7 "@vercel/kv": ^1.0.1 "@vercel/postgres-kysely": ^0.8.0 @@ -324,6 +325,7 @@ __metadata: kysely: ^0.27.3 next: ^13.2.0 node-fetch: ^3.3.0 + pg: ^8.12.0 postcss: ^8.4.21 prettier-plugin-tailwindcss: ^0.2.5 react: ^18.2.0 @@ -7407,6 +7409,17 @@ __metadata: languageName: node linkType: hard +"@types/pg@npm:^8.11.6": + version: 8.11.6 + resolution: "@types/pg@npm:8.11.6" + dependencies: + "@types/node": "*" + pg-protocol: "*" + pg-types: ^4.0.1 + checksum: 231f7e5bfe8b4d14cca398d24cd55f4f14f582f815b62059e6f3ee74108cf92089fbd946568ebc35fa402f238ed9c8a8c1e10e7084e83e4ca3aff75957243014 + languageName: node + linkType: hard + "@types/prop-types@npm:*": version: 15.7.9 resolution: "@types/prop-types@npm:15.7.9" @@ -18894,7 +18907,7 @@ __metadata: languageName: node linkType: hard -"obuf@npm:^1.0.0, obuf@npm:^1.1.2": +"obuf@npm:^1.0.0, obuf@npm:^1.1.2, obuf@npm:~1.1.2": version: 1.1.2 resolution: "obuf@npm:1.1.2" checksum: 41a2ba310e7b6f6c3b905af82c275bf8854896e2e4c5752966d64cbcd2f599cfffd5932006bcf3b8b419dfdacebb3a3912d5d94e10f1d0acab59876c8757f27f @@ -19341,6 +19354,20 @@ __metadata: languageName: node linkType: hard +"pg-cloudflare@npm:^1.1.1": + version: 1.1.1 + resolution: "pg-cloudflare@npm:1.1.1" + checksum: 32aac06b5dc4588bbf78801b6267781bc7e13be672009df949d08e9627ba9fdc26924916665d4de99d47f9b0495301930547488dad889d826856976c7b3f3731 + languageName: node + linkType: hard + +"pg-connection-string@npm:^2.6.4": + version: 2.6.4 + resolution: "pg-connection-string@npm:2.6.4" + checksum: 2c1d2ac1add1f93076f1594d217a0980f79add05dc48de6363e1c550827c78a6ee3e3b5420da9c54858f6b678cdb348aed49732ee68158b6cdb70f1d1c748cf9 + languageName: node + linkType: hard + "pg-int8@npm:1.0.1": version: 1.0.1 resolution: "pg-int8@npm:1.0.1" @@ -19348,14 +19375,30 @@ __metadata: languageName: node linkType: hard -"pg-protocol@npm:*": +"pg-numeric@npm:1.0.2": + version: 1.0.2 + resolution: "pg-numeric@npm:1.0.2" + checksum: 8899f8200caa1744439a8778a9eb3ceefb599d893e40a09eef84ee0d4c151319fd416634a6c0fc7b7db4ac268710042da5be700b80ef0de716fe089b8652c84f + languageName: node + linkType: hard + +"pg-pool@npm:^3.6.2": + version: 3.6.2 + resolution: "pg-pool@npm:3.6.2" + peerDependencies: + pg: ">=8.0" + checksum: 5ceee4320a35fce08777d085d50a30a1253574257e1e7c5c56c915056d387d340f797115580c8d90a46691f83c39a9b4da1fd810d9ad168cc455c79c289116f4 + languageName: node + linkType: hard + +"pg-protocol@npm:*, pg-protocol@npm:^1.6.1": version: 1.6.1 resolution: "pg-protocol@npm:1.6.1" checksum: cce3f72cc4bdc04db9ce3fa38b2c45b745f0a95a925847b349087f52c02c4d51b7c74d8867e40639699d0c7609accfaffb6b1d221b3268d2bdc4bb8d6a2995a3 languageName: node linkType: hard -"pg-types@npm:^2.2.0": +"pg-types@npm:^2.1.0, pg-types@npm:^2.2.0": version: 2.2.0 resolution: "pg-types@npm:2.2.0" dependencies: @@ -19368,6 +19411,52 @@ __metadata: languageName: node linkType: hard +"pg-types@npm:^4.0.1": + version: 4.0.2 + resolution: "pg-types@npm:4.0.2" + dependencies: + pg-int8: 1.0.1 + pg-numeric: 1.0.2 + postgres-array: ~3.0.1 + postgres-bytea: ~3.0.0 + postgres-date: ~2.1.0 + postgres-interval: ^3.0.0 + postgres-range: ^1.1.1 + checksum: c4b813382d4a75f87462fab3245d5422b86ba1a54a1b330e6b43a459c127b4d02553dc7e5b4ae4fa0f5f17971d416eb393810f69ff6d30d986e45c2f20778c55 + languageName: node + linkType: hard + +"pg@npm:^8.12.0": + version: 8.12.0 + resolution: "pg@npm:8.12.0" + dependencies: + pg-cloudflare: ^1.1.1 + pg-connection-string: ^2.6.4 + pg-pool: ^3.6.2 + pg-protocol: ^1.6.1 + pg-types: ^2.1.0 + pgpass: 1.x + peerDependencies: + pg-native: ">=3.0.1" + dependenciesMeta: + pg-cloudflare: + optional: true + peerDependenciesMeta: + pg-native: + optional: true + checksum: 8450b61c787f360e22182aa853548f834f13622714868d0789a60f63743d66ae28930cdca0ef0251bfc89b04679e9074c1398f172c2937bf59b5a360337f4149 + languageName: node + linkType: hard + +"pgpass@npm:1.x": + version: 1.0.5 + resolution: "pgpass@npm:1.0.5" + dependencies: + split2: ^4.1.0 + checksum: 947ac096c031eebdf08d989de2e9f6f156b8133d6858c7c2c06c041e1e71dda6f5f3bad3c0ec1e96a09497bbc6ef89e762eefe703b5ef9cb2804392ec52ec400 + languageName: node + linkType: hard + "picocolors@npm:^1.0.0": version: 1.0.0 resolution: "picocolors@npm:1.0.0" @@ -20050,6 +20139,13 @@ __metadata: languageName: node linkType: hard +"postgres-array@npm:~3.0.1": + version: 3.0.2 + resolution: "postgres-array@npm:3.0.2" + checksum: 5955f9dffeb6fa960c1a0b04fd4b2ba16813ddb636934ad26f902e4d76a91c0b743dcc6edc4cffc52deba7d547505e0020adea027c1d50a774f989cf955420d1 + languageName: node + linkType: hard + "postgres-bytea@npm:~1.0.0": version: 1.0.0 resolution: "postgres-bytea@npm:1.0.0" @@ -20057,6 +20153,15 @@ __metadata: languageName: node linkType: hard +"postgres-bytea@npm:~3.0.0": + version: 3.0.0 + resolution: "postgres-bytea@npm:3.0.0" + dependencies: + obuf: ~1.1.2 + checksum: 5f917a003fcaa0df7f285e1c37108ad474ce91193466b9bd4bcaecef2cdea98ca069c00aa6a8dbe6d2e7192336cadc3c9b36ae48d1555a299521918e00e2936b + languageName: node + linkType: hard + "postgres-date@npm:~1.0.4": version: 1.0.7 resolution: "postgres-date@npm:1.0.7" @@ -20064,6 +20169,13 @@ __metadata: languageName: node linkType: hard +"postgres-date@npm:~2.1.0": + version: 2.1.0 + resolution: "postgres-date@npm:2.1.0" + checksum: 5c573b0602e17c6134fd8bc8ac7689ac0302e1b199f15dd3578fc45186f206dbd0609f97bf0e4bd1db62234d7a37f29c04f4df525f7efebb9304363b2efca272 + languageName: node + linkType: hard + "postgres-interval@npm:^1.1.0": version: 1.2.0 resolution: "postgres-interval@npm:1.2.0" @@ -20073,6 +20185,20 @@ __metadata: languageName: node linkType: hard +"postgres-interval@npm:^3.0.0": + version: 3.0.0 + resolution: "postgres-interval@npm:3.0.0" + checksum: c7a1cf006de97de663b6b8c4d2b167aa9909a238c4866a94b15d303762f5ac884ff4796cd6e2111b7f0a91302b83c570453aa8506fd005b5a5d5dfa87441bebc + languageName: node + linkType: hard + +"postgres-range@npm:^1.1.1": + version: 1.1.4 + resolution: "postgres-range@npm:1.1.4" + checksum: 460af8c882a50e2c3d08ede5d5ee9e5e5a99dcf471e3ed55b4c17cad62dc85177b51bb8105b626a9c73de9edcba934e86665923b0d86e1c8e1f55d3e0f3530c6 + languageName: node + linkType: hard + "preact@npm:^10.12.0, preact@npm:^10.5.9": version: 10.18.1 resolution: "preact@npm:10.18.1" @@ -22528,7 +22654,7 @@ __metadata: languageName: node linkType: hard -"split2@npm:^4.0.0": +"split2@npm:^4.0.0, split2@npm:^4.1.0": version: 4.2.0 resolution: "split2@npm:4.2.0" checksum: 05d54102546549fe4d2455900699056580cca006c0275c334611420f854da30ac999230857a85fdd9914dc2109ae50f80fda43d2a445f2aa86eccdc1dfce779d