Merge pull request #86 from quantified-uncertainty/new-questions
feat: firstSeen, new questions in graphql
This commit is contained in:
commit
31555615a9
|
@ -0,0 +1,28 @@
|
||||||
|
-- questions
|
||||||
|
ALTER TABLE "questions"
|
||||||
|
ADD COLUMN "fetched" TIMESTAMP(6),
|
||||||
|
ADD COLUMN "first_seen" TIMESTAMP(6);
|
||||||
|
|
||||||
|
UPDATE "questions"
|
||||||
|
SET "fetched" = "timestamp", "first_seen" = "timestamp";
|
||||||
|
|
||||||
|
ALTER TABLE "questions"
|
||||||
|
ALTER COLUMN "fetched" SET NOT NULL,
|
||||||
|
ALTER COLUMN "first_seen" SET NOT NULL;
|
||||||
|
|
||||||
|
-- history
|
||||||
|
ALTER TABLE "history"
|
||||||
|
ADD COLUMN "fetched" TIMESTAMP(6);
|
||||||
|
|
||||||
|
UPDATE "history" SET "fetched" = "timestamp";
|
||||||
|
|
||||||
|
ALTER TABLE "history"
|
||||||
|
ALTER COLUMN "fetched" SET NOT NULL;
|
||||||
|
|
||||||
|
-- populate first_seen
|
||||||
|
UPDATE questions
|
||||||
|
SET "first_seen" = h.fs
|
||||||
|
FROM (
|
||||||
|
SELECT id, MIN(fetched) AS fs FROM history GROUP BY id
|
||||||
|
) as h
|
||||||
|
WHERE questions.id = h.id;
|
14
prisma/migrations/20220520195517_indices/migration.sql
Normal file
14
prisma/migrations/20220520195517_indices/migration.sql
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "history_platform_idx" ON "history"("platform");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "history_fetched_idx" ON "history"("fetched");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "questions_platform_idx" ON "questions"("platform");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "questions_fetched_idx" ON "questions"("fetched");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "questions_first_seen_idx" ON "questions"("first_seen");
|
|
@ -24,24 +24,6 @@ model Dashboard {
|
||||||
@@map("dashboards")
|
@@map("dashboards")
|
||||||
}
|
}
|
||||||
|
|
||||||
model History {
|
|
||||||
id String
|
|
||||||
idref String?
|
|
||||||
question Question? @relation(fields: [idref], references: [id], onDelete: SetNull, onUpdate: Restrict)
|
|
||||||
title String
|
|
||||||
url String
|
|
||||||
platform String
|
|
||||||
description String
|
|
||||||
options Json
|
|
||||||
timestamp DateTime @db.Timestamp(6)
|
|
||||||
qualityindicators Json
|
|
||||||
extra Json
|
|
||||||
pk Int @id @default(autoincrement())
|
|
||||||
|
|
||||||
@@index([id])
|
|
||||||
@@map("history")
|
|
||||||
}
|
|
||||||
|
|
||||||
model Question {
|
model Question {
|
||||||
/// E.g. "fantasyscotus-580"
|
/// E.g. "fantasyscotus-580"
|
||||||
id String @id
|
id String @id
|
||||||
|
@ -68,7 +50,9 @@ model Question {
|
||||||
// }
|
// }
|
||||||
// ]
|
// ]
|
||||||
options Json
|
options Json
|
||||||
timestamp DateTime @db.Timestamp(6)
|
timestamp DateTime @db.Timestamp(6) // deprecated
|
||||||
|
fetched DateTime @db.Timestamp(6)
|
||||||
|
firstSeen DateTime @map("first_seen") @db.Timestamp(6)
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// "numforecasts": 120,
|
// "numforecasts": 120,
|
||||||
|
@ -80,9 +64,33 @@ model Question {
|
||||||
onFrontpage FrontpageId?
|
onFrontpage FrontpageId?
|
||||||
history History[]
|
history History[]
|
||||||
|
|
||||||
|
@@index([platform])
|
||||||
|
@@index([fetched])
|
||||||
|
@@index([firstSeen])
|
||||||
@@map("questions")
|
@@map("questions")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model History {
|
||||||
|
id String
|
||||||
|
idref String?
|
||||||
|
question Question? @relation(fields: [idref], references: [id], onDelete: SetNull, onUpdate: Restrict)
|
||||||
|
title String
|
||||||
|
url String
|
||||||
|
platform String
|
||||||
|
description String
|
||||||
|
options Json
|
||||||
|
timestamp DateTime @db.Timestamp(6) // deprecated
|
||||||
|
fetched DateTime @db.Timestamp(6)
|
||||||
|
qualityindicators Json
|
||||||
|
extra Json
|
||||||
|
pk Int @id @default(autoincrement())
|
||||||
|
|
||||||
|
@@index([id])
|
||||||
|
@@index([platform])
|
||||||
|
@@index([fetched])
|
||||||
|
@@map("history")
|
||||||
|
}
|
||||||
|
|
||||||
model FrontpageId {
|
model FrontpageId {
|
||||||
question Question @relation(fields: [id], references: [id])
|
question Question @relation(fields: [id], references: [id])
|
||||||
id String @unique
|
id String @unique
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import { platforms } from "../platforms/registry";
|
import { getPlatforms } from "../platforms/registry";
|
||||||
import { executeJobByName } from "./jobs";
|
import { executeJobByName } from "./jobs";
|
||||||
|
|
||||||
/* Do everything */
|
/* Do everything */
|
||||||
export async function doEverything() {
|
export async function doEverything() {
|
||||||
let jobNames = [...platforms.map((platform) => platform.name), "algolia"];
|
let jobNames = [
|
||||||
|
...getPlatforms().map((platform) => platform.name),
|
||||||
|
"algolia",
|
||||||
|
];
|
||||||
|
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log("");
|
console.log("");
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { doEverything } from "../flow/doEverything";
|
import { doEverything } from "../flow/doEverything";
|
||||||
import { rebuildFrontpage } from "../frontpage";
|
import { rebuildFrontpage } from "../frontpage";
|
||||||
import { processPlatform } from "../platforms";
|
import { processPlatform } from "../platforms";
|
||||||
import { platforms } from "../platforms/registry";
|
import { getPlatforms } from "../platforms/registry";
|
||||||
import { rebuildAlgoliaDatabase } from "../utils/algolia";
|
import { rebuildAlgoliaDatabase } from "../utils/algolia";
|
||||||
import { sleep } from "../utils/sleep";
|
import { sleep } from "../utils/sleep";
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ interface Job<ArgNames extends string = ""> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const jobs: Job<string>[] = [
|
export const jobs: Job<string>[] = [
|
||||||
...platforms.map((platform) => ({
|
...getPlatforms().map((platform) => ({
|
||||||
name: platform.name,
|
name: platform.name,
|
||||||
message: `Download predictions from ${platform.name}`,
|
message: `Download predictions from ${platform.name}`,
|
||||||
...(platform.version === "v2" ? { args: platform.fetcherArgs } : {}),
|
...(platform.version === "v2" ? { args: platform.fetcherArgs } : {}),
|
||||||
|
|
|
@ -26,7 +26,7 @@ export async function rebuildFrontpage() {
|
||||||
AND questions.description != ''
|
AND questions.description != ''
|
||||||
AND JSONB_ARRAY_LENGTH(questions.options) > 0
|
AND JSONB_ARRAY_LENGTH(questions.options) > 0
|
||||||
GROUP BY questions.id
|
GROUP BY questions.id
|
||||||
HAVING COUNT(DISTINCT history.timestamp) >= 7
|
HAVING COUNT(DISTINCT history.fetched) >= 7
|
||||||
ORDER BY RANDOM() LIMIT 50
|
ORDER BY RANDOM() LIMIT 50
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
@ -79,7 +79,7 @@ export const givewellopenphil: Platform = {
|
||||||
const dataWithDate = data.map((datum: any) => ({
|
const dataWithDate = data.map((datum: any) => ({
|
||||||
...datum,
|
...datum,
|
||||||
platform: platformName,
|
platform: platformName,
|
||||||
timestamp: new Date("2021-02-23"),
|
// timestamp: new Date("2021-02-23"),
|
||||||
}));
|
}));
|
||||||
return dataWithDate;
|
return dataWithDate;
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,9 +2,8 @@ import axios from "axios";
|
||||||
|
|
||||||
import { Question } from "@prisma/client";
|
import { Question } from "@prisma/client";
|
||||||
|
|
||||||
import { prisma } from "../database/prisma";
|
import { AlgoliaQuestion, questionToAlgoliaQuestion } from "../utils/algolia";
|
||||||
import { AlgoliaQuestion } from "../utils/algolia";
|
import { FetchedQuestion, Platform, prepareQuestion, upsertSingleQuestion } from "./";
|
||||||
import { FetchedQuestion, Platform, prepareQuestion } from "./";
|
|
||||||
|
|
||||||
/* Definitions */
|
/* Definitions */
|
||||||
const searchEndpoint =
|
const searchEndpoint =
|
||||||
|
@ -55,10 +54,12 @@ async function search(query: string): Promise<AlgoliaQuestion[]> {
|
||||||
const models: any[] = response.data.hits;
|
const models: any[] = response.data.hits;
|
||||||
const mappedModels: AlgoliaQuestion[] = models.map((model) => {
|
const mappedModels: AlgoliaQuestion[] = models.map((model) => {
|
||||||
const q = modelToQuestion(model);
|
const q = modelToQuestion(model);
|
||||||
return {
|
return questionToAlgoliaQuestion({
|
||||||
...q,
|
...q,
|
||||||
timestamp: String(q.timestamp),
|
fetched: new Date(),
|
||||||
};
|
timestamp: new Date(),
|
||||||
|
firstSeen: new Date(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// filter for duplicates. Surprisingly common.
|
// filter for duplicates. Surprisingly common.
|
||||||
|
@ -76,12 +77,8 @@ async function search(query: string): Promise<AlgoliaQuestion[]> {
|
||||||
|
|
||||||
const fetchQuestion = async (id: number): Promise<Question> => {
|
const fetchQuestion = async (id: number): Promise<Question> => {
|
||||||
const response = await axios({ url: `${apiEndpoint}/spaces/${id}` });
|
const response = await axios({ url: `${apiEndpoint}/spaces/${id}` });
|
||||||
let q = modelToQuestion(response.data);
|
const q = modelToQuestion(response.data);
|
||||||
return await prisma.question.upsert({
|
return await upsertSingleQuestion(q);
|
||||||
where: { id: q.id },
|
|
||||||
create: q,
|
|
||||||
update: q,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const guesstimate: Platform & {
|
export const guesstimate: Platform & {
|
||||||
|
|
|
@ -28,9 +28,14 @@ export interface QualityIndicators {
|
||||||
|
|
||||||
export type FetchedQuestion = Omit<
|
export type FetchedQuestion = Omit<
|
||||||
Question,
|
Question,
|
||||||
"extra" | "qualityindicators" | "timestamp" | "platform" | "options"
|
| "extra"
|
||||||
|
| "qualityindicators"
|
||||||
|
| "fetched"
|
||||||
|
| "firstSeen"
|
||||||
|
| "timestamp"
|
||||||
|
| "platform"
|
||||||
|
| "options"
|
||||||
> & {
|
> & {
|
||||||
timestamp?: Date;
|
|
||||||
extra?: object; // required in DB but annoying to return empty; also this is slightly stricter than Prisma's JsonValue
|
extra?: object; // required in DB but annoying to return empty; also this is slightly stricter than Prisma's JsonValue
|
||||||
options: QuestionOption[]; // stronger type than Prisma's JsonValue
|
options: QuestionOption[]; // stronger type than Prisma's JsonValue
|
||||||
qualityindicators: Omit<QualityIndicators, "stars">; // slightly stronger type than Prisma's JsonValue
|
qualityindicators: Omit<QualityIndicators, "stars">; // slightly stronger type than Prisma's JsonValue
|
||||||
|
@ -78,8 +83,14 @@ export type Platform<ArgNames extends string = ""> = {
|
||||||
// So here we build a new type which should be ok to use both in place of prisma's Question type and as an input to its update or create methods.
|
// So here we build a new type which should be ok to use both in place of prisma's Question type and as an input to its update or create methods.
|
||||||
type PreparedQuestion = Omit<
|
type PreparedQuestion = Omit<
|
||||||
Question,
|
Question,
|
||||||
"extra" | "qualityindicators" | "options"
|
| "extra"
|
||||||
|
| "qualityindicators"
|
||||||
|
| "options"
|
||||||
|
| "fetched"
|
||||||
|
| "firstSeen"
|
||||||
|
| "timestamp"
|
||||||
> & {
|
> & {
|
||||||
|
fetched: Date;
|
||||||
extra: NonNullable<Question["extra"]>;
|
extra: NonNullable<Question["extra"]>;
|
||||||
qualityindicators: NonNullable<Question["qualityindicators"]>;
|
qualityindicators: NonNullable<Question["qualityindicators"]>;
|
||||||
options: NonNullable<Question["options"]>;
|
options: NonNullable<Question["options"]>;
|
||||||
|
@ -91,8 +102,8 @@ export const prepareQuestion = (
|
||||||
): PreparedQuestion => {
|
): PreparedQuestion => {
|
||||||
return {
|
return {
|
||||||
extra: {},
|
extra: {},
|
||||||
timestamp: new Date(),
|
|
||||||
...q,
|
...q,
|
||||||
|
fetched: new Date(),
|
||||||
platform: platform.name,
|
platform: platform.name,
|
||||||
qualityindicators: {
|
qualityindicators: {
|
||||||
...q.qualityindicators,
|
...q.qualityindicators,
|
||||||
|
@ -101,6 +112,21 @@ export const prepareQuestion = (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const upsertSingleQuestion = async (
|
||||||
|
q: PreparedQuestion
|
||||||
|
): Promise<Question> => {
|
||||||
|
return await prisma.question.upsert({
|
||||||
|
where: { id: q.id },
|
||||||
|
create: {
|
||||||
|
...q,
|
||||||
|
firstSeen: new Date(),
|
||||||
|
timestamp: q.fetched, // deprecated
|
||||||
|
},
|
||||||
|
update: q,
|
||||||
|
});
|
||||||
|
// TODO - update history?
|
||||||
|
};
|
||||||
|
|
||||||
export const processPlatform = async <T extends string = "">(
|
export const processPlatform = async <T extends string = "">(
|
||||||
platform: Platform<T>,
|
platform: Platform<T>,
|
||||||
args?: { [k in T]: string }
|
args?: { [k in T]: string }
|
||||||
|
@ -144,9 +170,9 @@ export const processPlatform = async <T extends string = "">(
|
||||||
|
|
||||||
for (const q of fetchedQuestions.map((q) => prepareQuestion(q, platform))) {
|
for (const q of fetchedQuestions.map((q) => prepareQuestion(q, platform))) {
|
||||||
if (oldIdsSet.has(q.id)) {
|
if (oldIdsSet.has(q.id)) {
|
||||||
|
// TODO - check if question has changed for better performance
|
||||||
updatedQuestions.push(q);
|
updatedQuestions.push(q);
|
||||||
} else {
|
} else {
|
||||||
// TODO - check if question has changed for better performance
|
|
||||||
createdQuestions.push(q);
|
createdQuestions.push(q);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,7 +180,11 @@ export const processPlatform = async <T extends string = "">(
|
||||||
const stats: { created?: number; updated?: number; deleted?: number } = {};
|
const stats: { created?: number; updated?: number; deleted?: number } = {};
|
||||||
|
|
||||||
await prisma.question.createMany({
|
await prisma.question.createMany({
|
||||||
data: createdQuestions,
|
data: createdQuestions.map((q) => ({
|
||||||
|
...q,
|
||||||
|
firstSeen: new Date(),
|
||||||
|
timestamp: q.fetched, // deprecated
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
stats.created = createdQuestions.length;
|
stats.created = createdQuestions.length;
|
||||||
|
|
||||||
|
@ -181,6 +211,7 @@ export const processPlatform = async <T extends string = "">(
|
||||||
await prisma.history.createMany({
|
await prisma.history.createMany({
|
||||||
data: [...createdQuestions, ...updatedQuestions].map((q) => ({
|
data: [...createdQuestions, ...updatedQuestions].map((q) => ({
|
||||||
...q,
|
...q,
|
||||||
|
timestamp: q.fetched, // deprecated
|
||||||
idref: q.id,
|
idref: q.id,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,7 +17,9 @@ import { smarkets } from "./smarkets";
|
||||||
import { wildeford } from "./wildeford";
|
import { wildeford } from "./wildeford";
|
||||||
import { xrisk } from "./xrisk";
|
import { xrisk } from "./xrisk";
|
||||||
|
|
||||||
export const platforms: Platform<string>[] = [
|
// function instead of const array, this helps to fight circular dependencies
|
||||||
|
export const getPlatforms = (): Platform<string>[] => {
|
||||||
|
return [
|
||||||
betfair,
|
betfair,
|
||||||
fantasyscotus,
|
fantasyscotus,
|
||||||
foretold,
|
foretold,
|
||||||
|
@ -35,12 +37,23 @@ export const platforms: Platform<string>[] = [
|
||||||
smarkets,
|
smarkets,
|
||||||
wildeford,
|
wildeford,
|
||||||
xrisk,
|
xrisk,
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
let _nameToLabelCache: { [k: string]: string } | undefined;
|
||||||
|
export function platformNameToLabel(name: string): string {
|
||||||
|
if (!_nameToLabelCache) {
|
||||||
|
_nameToLabelCache = Object.fromEntries(
|
||||||
|
getPlatforms().map((platform) => [platform.name, platform.label])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _nameToLabelCache[name] || name;
|
||||||
|
}
|
||||||
|
|
||||||
// get frontend-safe version of platforms data
|
// get frontend-safe version of platforms data
|
||||||
|
|
||||||
export const getPlatformsConfig = (): PlatformConfig[] => {
|
export const getPlatformsConfig = (): PlatformConfig[] => {
|
||||||
const platformsConfig = platforms.map((platform) => ({
|
const platformsConfig = getPlatforms().map((platform) => ({
|
||||||
name: platform.name,
|
name: platform.name,
|
||||||
label: platform.label,
|
label: platform.label,
|
||||||
color: platform.color,
|
color: platform.color,
|
||||||
|
|
|
@ -162,7 +162,6 @@ async function processEventMarkets(event: any, ctx: Context) {
|
||||||
url: "https://smarkets.com/event/" + market.event_id + market.slug,
|
url: "https://smarkets.com/event/" + market.event_id + market.slug,
|
||||||
description: market.description,
|
description: market.description,
|
||||||
options,
|
options,
|
||||||
timestamp: new Date(),
|
|
||||||
qualityindicators: {},
|
qualityindicators: {},
|
||||||
extra: {
|
extra: {
|
||||||
contracts,
|
contracts,
|
||||||
|
|
|
@ -96,7 +96,8 @@ async function processPredictions(
|
||||||
url: prediction["url"],
|
url: prediction["url"],
|
||||||
description: prediction["Notes"] || "",
|
description: prediction["Notes"] || "",
|
||||||
options,
|
options,
|
||||||
timestamp: new Date(Date.parse(prediction["Prediction Date"] + "Z")),
|
//// TODO - use `created` field for this
|
||||||
|
// timestamp: new Date(Date.parse(prediction["Prediction Date"] + "Z")),
|
||||||
qualityindicators: {},
|
qualityindicators: {},
|
||||||
};
|
};
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -3,16 +3,23 @@ import algoliasearch from "algoliasearch";
|
||||||
import { Question } from "@prisma/client";
|
import { Question } from "@prisma/client";
|
||||||
|
|
||||||
import { prisma } from "../database/prisma";
|
import { prisma } from "../database/prisma";
|
||||||
import { platforms } from "../platforms/registry";
|
import { platformNameToLabel } from "../platforms/registry";
|
||||||
|
|
||||||
let cookie = process.env.ALGOLIA_MASTER_API_KEY || "";
|
const cookie = process.env.ALGOLIA_MASTER_API_KEY || "";
|
||||||
const algoliaAppId = process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "";
|
const algoliaAppId = process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "";
|
||||||
const client = algoliasearch(algoliaAppId, cookie);
|
const client = algoliasearch(algoliaAppId, cookie);
|
||||||
const index = client.initIndex("metaforecast");
|
const index = client.initIndex("metaforecast");
|
||||||
|
|
||||||
export type AlgoliaQuestion = Omit<Question, "timestamp"> & {
|
export type AlgoliaQuestion = Omit<
|
||||||
timestamp: string;
|
Question,
|
||||||
|
"fetched" | "firstSeen" | "timestamp"
|
||||||
|
> & {
|
||||||
|
timestamp?: string; // deprecated
|
||||||
|
fetched?: string;
|
||||||
|
firstSeen?: string;
|
||||||
optionsstringforsearch?: string;
|
optionsstringforsearch?: string;
|
||||||
|
platformLabel: string;
|
||||||
|
objectID: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getoptionsstringforsearch = (record: Question): string => {
|
const getoptionsstringforsearch = (record: Question): string => {
|
||||||
|
@ -26,23 +33,24 @@ const getoptionsstringforsearch = (record: Question): string => {
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const questionToAlgoliaQuestion = (
|
||||||
|
question: Question
|
||||||
|
): AlgoliaQuestion => {
|
||||||
|
return {
|
||||||
|
...question,
|
||||||
|
fetched: question.fetched.toISOString(),
|
||||||
|
timestamp: question.timestamp.toISOString(), // deprecated
|
||||||
|
firstSeen: question.firstSeen.toISOString(),
|
||||||
|
platformLabel: platformNameToLabel(question.platform),
|
||||||
|
objectID: question.id,
|
||||||
|
optionsstringforsearch: getoptionsstringforsearch(question),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export async function rebuildAlgoliaDatabase() {
|
export async function rebuildAlgoliaDatabase() {
|
||||||
const questions = await prisma.question.findMany();
|
const questions = await prisma.question.findMany();
|
||||||
|
|
||||||
const platformNameToLabel = Object.fromEntries(
|
const records: AlgoliaQuestion[] = questions.map(questionToAlgoliaQuestion);
|
||||||
platforms.map((platform) => [platform.name, platform.label])
|
|
||||||
);
|
|
||||||
|
|
||||||
const records: AlgoliaQuestion[] = questions.map(
|
|
||||||
(question, index: number) => ({
|
|
||||||
...question,
|
|
||||||
timestamp: `${question.timestamp}`,
|
|
||||||
platformLabel:
|
|
||||||
platformNameToLabel[question.platform] || question.platform,
|
|
||||||
objectID: index,
|
|
||||||
optionsstringforsearch: getoptionsstringforsearch(question),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (await index.exists()) {
|
if (await index.exists()) {
|
||||||
console.log("Index exists");
|
console.log("Index exists");
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { prisma } from "../../backend/database/prisma";
|
import { prisma } from "../../backend/database/prisma";
|
||||||
import { platforms } from "../../backend/platforms/registry";
|
import { getPlatforms } from "../../backend/platforms/registry";
|
||||||
import { builder } from "../builder";
|
import { builder } from "../builder";
|
||||||
|
|
||||||
export const PlatformObj = builder.objectRef<string>("Platform").implement({
|
export const PlatformObj = builder.objectRef<string>("Platform").implement({
|
||||||
|
@ -20,7 +20,7 @@ export const PlatformObj = builder.objectRef<string>("Platform").implement({
|
||||||
return "Guesstimate";
|
return "Guesstimate";
|
||||||
}
|
}
|
||||||
// kinda slow and repetitive, TODO - store a map {name => platform} somewhere and `getPlatform` util function?
|
// kinda slow and repetitive, TODO - store a map {name => platform} somewhere and `getPlatform` util function?
|
||||||
const platform = platforms.find((p) => p.name === platformName);
|
const platform = getPlatforms().find((p) => p.name === platformName);
|
||||||
if (!platform) {
|
if (!platform) {
|
||||||
throw new Error(`Unknown platform ${platformName}`);
|
throw new Error(`Unknown platform ${platformName}`);
|
||||||
}
|
}
|
||||||
|
@ -36,10 +36,10 @@ export const PlatformObj = builder.objectRef<string>("Platform").implement({
|
||||||
platform: platformName,
|
platform: platformName,
|
||||||
},
|
},
|
||||||
_max: {
|
_max: {
|
||||||
timestamp: true,
|
fetched: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return res._max.timestamp;
|
return res._max.fetched;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
@ -49,7 +49,7 @@ builder.queryField("platforms", (t) =>
|
||||||
t.field({
|
t.field({
|
||||||
type: [PlatformObj],
|
type: [PlatformObj],
|
||||||
resolve: async (parent, args) => {
|
resolve: async (parent, args) => {
|
||||||
return platforms.map((platform) => platform.name);
|
return getPlatforms().map((platform) => platform.name);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -70,8 +70,21 @@ const QuestionShapeInterface = builder
|
||||||
}),
|
}),
|
||||||
timestamp: t.field({
|
timestamp: t.field({
|
||||||
type: "Date",
|
type: "Date",
|
||||||
description: "Timestamp at which metaforecast fetched the question",
|
description:
|
||||||
resolve: (parent) => parent.timestamp,
|
"Last timestamp at which metaforecast fetched the question",
|
||||||
|
deprecationReason: "Renamed to `fetched`",
|
||||||
|
resolve: (parent) => parent.fetched,
|
||||||
|
}),
|
||||||
|
fetched: t.field({
|
||||||
|
type: "Date",
|
||||||
|
description:
|
||||||
|
"Last timestamp at which metaforecast fetched the question",
|
||||||
|
resolve: (parent) => parent.fetched,
|
||||||
|
}),
|
||||||
|
fetchedStr: t.string({
|
||||||
|
description:
|
||||||
|
"Last timestamp at which metaforecast fetched the question, in ISO 8601 format",
|
||||||
|
resolve: (parent) => parent.fetched.toISOString(),
|
||||||
}),
|
}),
|
||||||
qualityIndicators: t.field({
|
qualityIndicators: t.field({
|
||||||
type: QualityIndicatorsObj,
|
type: QualityIndicatorsObj,
|
||||||
|
@ -114,10 +127,20 @@ export const QuestionObj = builder.prismaObject("Question", {
|
||||||
resolve: (parent) => (parent.extra as any)?.visualization, // used for guesstimate only, see searchGuesstimate.ts
|
resolve: (parent) => (parent.extra as any)?.visualization, // used for guesstimate only, see searchGuesstimate.ts
|
||||||
nullable: true,
|
nullable: true,
|
||||||
}),
|
}),
|
||||||
|
firstSeen: t.field({
|
||||||
|
type: "Date",
|
||||||
|
description: "First timestamp at which metaforecast fetched the question",
|
||||||
|
resolve: (parent) => parent.firstSeen,
|
||||||
|
}),
|
||||||
|
firstSeenStr: t.string({
|
||||||
|
description:
|
||||||
|
"First timestamp at which metaforecast fetched the question, in ISO 8601 format",
|
||||||
|
resolve: (parent) => parent.firstSeen.toISOString(),
|
||||||
|
}),
|
||||||
history: t.relation("history", {
|
history: t.relation("history", {
|
||||||
query: () => ({
|
query: () => ({
|
||||||
orderBy: {
|
orderBy: {
|
||||||
timestamp: "asc",
|
fetched: "asc",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
@ -130,7 +153,21 @@ builder.queryField("questions", (t) =>
|
||||||
type: "Question",
|
type: "Question",
|
||||||
cursor: "id",
|
cursor: "id",
|
||||||
maxSize: 1000,
|
maxSize: 1000,
|
||||||
resolve: (query) => prisma.question.findMany({ ...query }),
|
args: {
|
||||||
|
orderBy: t.arg({
|
||||||
|
type: builder.enumType("QuestionsOrderBy", {
|
||||||
|
values: ["FIRST_SEEN_DESC"] as const,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
resolve: (query, parent, args) => {
|
||||||
|
return prisma.question.findMany({
|
||||||
|
...query,
|
||||||
|
...(args.orderBy === "FIRST_SEEN_DESC"
|
||||||
|
? { orderBy: [{ firstSeen: "desc" }, { id: "asc" }] }
|
||||||
|
: {}), // TODO - explicit default order?
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{}
|
{}
|
||||||
|
|
|
@ -63,7 +63,15 @@ builder.queryField("searchQuestions", (t) =>
|
||||||
|
|
||||||
return results.map((q) => ({
|
return results.map((q) => ({
|
||||||
...q,
|
...q,
|
||||||
timestamp: new Date(q.timestamp),
|
fetched: new Date(
|
||||||
|
q.fetched || q.timestamp || new Date().toISOString() // q.timestamp is deprecated, TODO - just use `q.fetched`
|
||||||
|
),
|
||||||
|
timestamp: new Date(
|
||||||
|
q.fetched || q.timestamp || new Date().toISOString()
|
||||||
|
),
|
||||||
|
firstSeen: new Date(
|
||||||
|
q.firstSeen || new Date().toISOString() // TODO - q.firstSeen is not yet populated in algolia
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { GetServerSideProps, NextPage } from "next";
|
import { GetServerSideProps, NextPage } from "next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { getPlatformsConfig, platforms } from "../backend/platforms/registry";
|
import { getPlatforms, getPlatformsConfig } from "../backend/platforms/registry";
|
||||||
import { Layout } from "../web/common/Layout";
|
import { Layout } from "../web/common/Layout";
|
||||||
import { Props, QueryParameters, SearchScreen } from "../web/search/components/SearchScreen";
|
import { Props, QueryParameters, SearchScreen } from "../web/search/components/SearchScreen";
|
||||||
import { FrontpageDocument, SearchDocument } from "../web/search/queries.generated";
|
import { FrontpageDocument, SearchDocument } from "../web/search/queries.generated";
|
||||||
|
@ -19,7 +19,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
query: "",
|
query: "",
|
||||||
starsThreshold: 2,
|
starsThreshold: 2,
|
||||||
forecastsThreshold: 0,
|
forecastsThreshold: 0,
|
||||||
forecastingPlatforms: platforms.map((platform) => platform.name),
|
forecastingPlatforms: getPlatforms().map((platform) => platform.name),
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialQueryParameters: QueryParameters = {
|
const initialQueryParameters: QueryParameters = {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { GetServerSideProps, NextPage } from "next";
|
import { GetServerSideProps, NextPage } from "next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { platforms } from "../backend/platforms/registry";
|
import { getPlatforms } from "../backend/platforms/registry";
|
||||||
import { QuestionFragment } from "../web/fragments.generated";
|
import { QuestionFragment } from "../web/fragments.generated";
|
||||||
import { QuestionCard } from "../web/questions/components/QuestionCard";
|
import { QuestionCard } from "../web/questions/components/QuestionCard";
|
||||||
import { SearchDocument } from "../web/search/queries.generated";
|
import { SearchDocument } from "../web/search/queries.generated";
|
||||||
|
@ -23,7 +23,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
query: "",
|
query: "",
|
||||||
starsThreshold: 2,
|
starsThreshold: 2,
|
||||||
forecastsThreshold: 0,
|
forecastsThreshold: 0,
|
||||||
forecastingPlatforms: platforms.map((platform) => platform.name),
|
forecastingPlatforms: getPlatforms().map((platform) => platform.name),
|
||||||
...urlQuery,
|
...urlQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -96,6 +96,7 @@ export default async function searchWithAlgolia({
|
||||||
title: "No search results match your query",
|
title: "No search results match your query",
|
||||||
url: "https://metaforecast.org",
|
url: "https://metaforecast.org",
|
||||||
platform: "metaforecast",
|
platform: "metaforecast",
|
||||||
|
platformLabel: "metaforecast",
|
||||||
description: "Maybe try a broader query?",
|
description: "Maybe try a broader query?",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
|
@ -109,7 +110,7 @@ export default async function searchWithAlgolia({
|
||||||
type: "PROBABILITY",
|
type: "PROBABILITY",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
timestamp: `${new Date().toISOString().slice(0, 10)}`,
|
fetched: new Date().toISOString(),
|
||||||
qualityindicators: {
|
qualityindicators: {
|
||||||
numforecasts: 1,
|
numforecasts: 1,
|
||||||
numforecasters: 1,
|
numforecasters: 1,
|
||||||
|
@ -126,8 +127,10 @@ export default async function searchWithAlgolia({
|
||||||
title: `Did you mean: ${queryString}?`,
|
title: `Did you mean: ${queryString}?`,
|
||||||
url: "https://metaforecast.org/recursion?bypassEasterEgg=true",
|
url: "https://metaforecast.org/recursion?bypassEasterEgg=true",
|
||||||
platform: "metaforecast",
|
platform: "metaforecast",
|
||||||
|
platformLabel: "metaforecast",
|
||||||
description:
|
description:
|
||||||
"Fatal error: Too much recursion. Click to proceed anyways",
|
"Fatal error: Too much recursion. Click to proceed anyways",
|
||||||
|
fetched: new Date().toISOString(),
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: "Yes",
|
name: "Yes",
|
||||||
|
@ -140,7 +143,6 @@ export default async function searchWithAlgolia({
|
||||||
type: "PROBABILITY",
|
type: "PROBABILITY",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
timestamp: `${new Date().toISOString().slice(0, 10)}`,
|
|
||||||
qualityindicators: {
|
qualityindicators: {
|
||||||
numforecasts: 1,
|
numforecasts: 1,
|
||||||
numforecasters: 1,
|
numforecasters: 1,
|
||||||
|
@ -161,6 +163,7 @@ export default async function searchWithAlgolia({
|
||||||
title: "No search results appear to match your query",
|
title: "No search results appear to match your query",
|
||||||
url: "https://metaforecast.org",
|
url: "https://metaforecast.org",
|
||||||
platform: "metaforecast",
|
platform: "metaforecast",
|
||||||
|
platformLabel: "metaforecast",
|
||||||
description: "Maybe try a broader query? That said, we could be wrong.",
|
description: "Maybe try a broader query? That said, we could be wrong.",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
|
@ -174,7 +177,7 @@ export default async function searchWithAlgolia({
|
||||||
type: "PROBABILITY",
|
type: "PROBABILITY",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
timestamp: `${new Date().toISOString().slice(0, 10)}`,
|
fetched: new Date().toISOString(),
|
||||||
qualityindicators: {
|
qualityindicators: {
|
||||||
numforecasts: 1,
|
numforecasts: 1,
|
||||||
numforecasters: 1,
|
numforecasters: 1,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user