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

Migrate collection and category queries to data layer #2739

Merged
merged 16 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
7 changes: 5 additions & 2 deletions packages/data-layer/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["prettier", "@typescript-eslint", "import"],
"plugins": ["prettier", "@typescript-eslint", "import", "vitest"],
"extends": [
"eslint:recommended",
"prettier",
"plugin:@typescript-eslint/recommended"
"plugin:@typescript-eslint/recommended",
"plugin:vitest/recommended"
],
"env": {
"node": true
Expand All @@ -17,6 +18,8 @@
"import/no-default-export": "error",
"no-console": "error",
"no-process-env": "error",
"vitest/no-disabled-tests": "error",
"vitest/no-focused-tests": "error",
"@typescript-eslint/no-unused-vars": [1, { "argsIgnorePattern": "^_" }],
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/no-non-null-assertion": "error",
Expand Down
4 changes: 3 additions & 1 deletion packages/data-layer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"types": "dist/index.d.ts",
"scripts": {
"dev": "tsx watch -r dotenv-flow/config src/index.ts",
"start": "node -r dotenv-flow/config dist/index.js",
"lint": "eslint --cache --max-warnings=0",
"lint:fix": "eslint --cache --max-warnings=0 --fix",
"lint:all:fix": "eslint --fix --cache --max-warnings=0 src",
Expand All @@ -29,6 +28,7 @@
"@types/debug": "^4.1.8",
"@types/knuth-shuffle-seeded": "^1.0.1",
"@types/node": "^20.6.2",
"@types/papaparse": "^5.3.14",
"@types/react": "^18.1.0",
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.7.0",
Expand All @@ -37,6 +37,7 @@
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vitest": "^0.3.10",
"leasot": "^13.3.0",
"lefthook": "^1.4.11",
"prettier": "^3.0.3",
Expand All @@ -56,6 +57,7 @@
"debug": "^4.3.4",
"dotenv-flow": "^3.3.0",
"knuth-shuffle-seeded": "^1.0.6",
"papaparse": "^5.4.1",
"ts-custom-error": "^3.3.1",
"ts-essentials": "^9.4.1",
"viem": "^1.5.3",
Expand Down
48 changes: 48 additions & 0 deletions packages/data-layer/src/backends/categories.data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { SearchBasedProjectCategory } from "../data.types";

export const CATEGORIES_HARDCODED: SearchBasedProjectCategory[] = [
{
id: "open-source",
name: "Open source",
images: [
"/assets/categories/category_01.jpg",
"/assets/categories/category_02.jpg",
"/assets/categories/category_03.jpg",
"/assets/categories/category_04.jpg",
],
searchQuery: "open source, open source software",
},
{
id: "education",
name: "Education",
images: [
"/assets/categories/category_05.jpg",
"/assets/categories/category_06.jpg",
"/assets/categories/category_07.jpg",
"/assets/categories/category_08.jpg",
],
searchQuery: "education, teaching",
},
{
id: "civic-engagement",
name: "Civic engagement",
images: [
"/assets/categories/category_09.jpg",
"/assets/categories/category_10.jpg",
"/assets/categories/category_11.jpg",
"/assets/categories/category_12.jpg",
],
searchQuery: "civic engagement, civics, governance, democracy ",
},
{
id: "social",
name: "Social",
images: [
"/assets/categories/category_13.jpg",
"/assets/categories/category_14.jpg",
"/assets/categories/category_15.jpg",
"/assets/categories/category_16.jpg",
],
searchQuery: "social",
},
];
14 changes: 14 additions & 0 deletions packages/data-layer/src/backends/categories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SearchBasedProjectCategory } from "../data.types";
import { CATEGORIES_HARDCODED } from "./categories.data";

export const getSearchBasedCategories = (): Promise<
SearchBasedProjectCategory[]
> => {
return Promise.resolve(CATEGORIES_HARDCODED);
};

export const getSearchBasedCategoryById = (
id: string,
): Promise<SearchBasedProjectCategory | null> => {
return Promise.resolve(CATEGORIES_HARDCODED.find((c) => c.id === id) ?? null);
};
1,221 changes: 1,221 additions & 0 deletions packages/data-layer/src/backends/collections.data.ts

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions packages/data-layer/src/backends/collections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { z } from "zod";
import { getAddress } from "viem";
import Papa from "papaparse";
import { Collection } from "../data.types";
import { COLLECTIONS_HARDCODED } from "./collections.data";

export type CollectionsSource =
| { type: "hardcoded" }
| { type: "google-sheet"; url: string };

const SPREADSHEET_SCHEMA = z.array(
z.object({
id: z.string(),
author: z.string(),
name: z.string(),
description: z.string(),
images: z.string(),
applicationRefs: z.string(),
}),
);

export const getCollections = async (opts?: {
source: CollectionsSource;
}): Promise<Collection[]> => {
if (opts?.source.type === "google-sheet") {
try {
const res = await fetch(
opts.source.url,
// "https://docs.google.com/spreadsheets/d/e/2PACX-1vR-hsia6fcd6bYOKrQCxNHtDX_WcxYTnXMXxVpdCbpzZN8udV0juCjb6cnsx-RraBS9tkJm2sl1mqcP/pub?gid=0&single=true&output=tsv",
);

if (!res.ok) {
throw new Error("Error loading collections");
}

const { data: rawData } = Papa.parse(await res.text(), {
delimiter: "\t",
header: true,
});

const data = SPREADSHEET_SCHEMA.parse(rawData);

const collections = data.map((collection) => ({
id: collection.id,
name: collection.name,
description: collection.description,
author: collection.author,
images: collection.images.split(/\s+/),
applicationRefs: collection.applicationRefs.split(/\s+/),
}));

return collections;
} catch (err) {
// Return hardcoded collections in case there is an error accessing online
// data. TODO review this choice (should we log the error? should we
// surface the error? should we return empty collections?)
return COLLECTIONS_HARDCODED.map(ensureAddressIsInChecksumFormat);
}
} else {
return COLLECTIONS_HARDCODED.map(ensureAddressIsInChecksumFormat);
}
};

export const getCollectionById = async (
id: string,
opts?: { source: CollectionsSource },
): Promise<Collection | null> => {
const collections = await getCollections(opts);
return collections.find((c) => c.id === id) ?? null;
};

const ensureAddressIsInChecksumFormat = (
collection: Collection,
): Collection => ({
...collection,
applicationRefs: collection.applicationRefs.map((applicationRef) => {
const [chain, address, idx] = applicationRef.split(":");
return [chain, getAddress(address), idx].join(":");
}),
});
10 changes: 5 additions & 5 deletions packages/data-layer/src/backends/legacy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe("legacy", () => {
"115792089237316195423570985008687907853269984665640564039457584007913129639935",
},
])[0].roundStartTime,
).toBe(undefined);
).toBeUndefined();
});

test("sortRounds", async () => {
Expand All @@ -55,8 +55,8 @@ describe("legacy", () => {
orderDirection: "asc",
},
);
expect(sortedAsc[0].matchAmount).toEqual("1");
expect(sortedAsc[1].matchAmount).toEqual("10");
expect(sortedAsc[0].matchAmount).toBe("1");
expect(sortedAsc[1].matchAmount).toBe("10");

const sortedDesc = sortRounds(
[{ matchAmount: "10" }, { matchAmount: "1" }] as RoundOverview[],
Expand All @@ -66,8 +66,8 @@ describe("legacy", () => {
orderDirection: "desc",
},
);
expect(sortedDesc[0].matchAmount).toEqual("10");
expect(sortedDesc[1].matchAmount).toEqual("1");
expect(sortedDesc[0].matchAmount).toBe("10");
expect(sortedDesc[1].matchAmount).toBe("1");
});
});

Expand Down
115 changes: 112 additions & 3 deletions packages/data-layer/src/data-layer.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { getAddress } from "viem";
import { describe, test, expect, vi } from "vitest";
import { DataLayer } from "./data-layer";
import { PassportVerifier } from "@gitcoinco/passport-sdk-verifier";
import { VerifiableCredential } from "@gitcoinco/passport-sdk-types";
import { DataLayer } from "./data-layer";

describe("applications search", () => {
describe("can retrieve multiple applications by search query", () => {
Expand Down Expand Up @@ -464,7 +465,7 @@ describe("applications search", () => {
});

describe("passport verification", () => {
test("invokes passport verifier ", async () => {
test("invokes passport verifier", async () => {
const mockPassportVerifier = {
verifyCredential: vi.fn().mockResolvedValue(true),
} as unknown as PassportVerifier;
Expand All @@ -486,7 +487,7 @@ describe("passport verification", () => {
} as VerifiableCredential,
});

expect(isVerified).toEqual(true);
expect(isVerified).toBe(true);
expect(mockPassportVerifier.verifyCredential).toBeCalledWith({
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
Expand All @@ -496,3 +497,111 @@ describe("passport verification", () => {
});
});
});

describe("categories", () => {
test("can be retrieved collectively", async () => {
const dataLayer = new DataLayer({
search: { baseUrl: "https://example.com" },
});

const { categories } = await dataLayer.query({
type: "search-based-project-categories",
});

expect(categories[0]).toMatchObject({
id: expect.any(String),
name: expect.any(String),
images: expect.any(Array),
searchQuery: expect.any(String),
});
expect(categories).toHaveLength(4);
});

test("can be retrieved individually", async () => {
const dataLayer = new DataLayer({
search: { baseUrl: "https://example.com" },
});

const { category } = await dataLayer.query({
type: "search-based-project-category",
id: "open-source",
});

expect(category).toMatchObject({
id: "open-source",
name: "Open source",
images: [
"/assets/categories/category_01.jpg",
"/assets/categories/category_02.jpg",
"/assets/categories/category_03.jpg",
"/assets/categories/category_04.jpg",
],
searchQuery: "open source, open source software",
});
});
});

describe("collections", () => {
test("can be retrieved collectively", async () => {
const dataLayer = new DataLayer({
search: { baseUrl: "https://example.com" },
});

const { collections } = await dataLayer.query({
type: "project-collections",
});

expect(collections[0]).toMatchObject({
id: expect.any(String),
author: expect.any(String),
images: expect.any(Array),
description: expect.any(String),
applicationRefs: expect.any(Array),
});
expect(collections).toHaveLength(12);
});

test("can be retrieved individually", async () => {
const dataLayer = new DataLayer({
search: { baseUrl: "https://example.com" },
});

const { collection } = await dataLayer.query({
type: "project-collection",
id: "first-time-grantees",
});

expect(collection).toMatchObject({
id: "first-time-grantees",
author: "Gitcoin",
name: "First Time Grantees",
images: [
// TODO: make into absolute URLs
"/assets/collections/collection_01.jpg",
"/assets/collections/collection_02.jpg",
"/assets/collections/collection_03.jpg",
"/assets/collections/collection_04.jpg",
],
description:
"This collection showcases all grantees in GG19 that have not participated in a past round on Grants Stack! Give these first-time grantees some love (and maybe some donations, too!).",
applicationRefs: expect.any(Array),
});
});

test("ensures that the address component of the application ref is in checksummed format", async () => {
const dataLayer = new DataLayer({
search: { baseUrl: "https://example.com" },
});

const { collections } = await dataLayer.query({
type: "project-collections",
});

for (const collection of collections) {
for (const applicationRef of collection.applicationRefs) {
const address = applicationRef.split(":")[1];
expect(address).toEqual(getAddress(address));
}
}
});
});
Loading