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

Promote to main #39

Merged
merged 8 commits into from
Oct 3, 2024
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
ARG NODE_VERSION
FROM node:${NODE_VERSION}-alpine3.20 AS base
FROM node:20.13.1-alpine3.20 AS base
RUN apk update && apk add --no-cache bash dumb-init

WORKDIR /usr/src/app
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"type": "module",
"scripts": {
"build": "tsc",
"docker:build": "docker build --build-arg NODE_VERSION=$(< .nvmrc) .",
"watch": "tsc --watch",
"start": "node dist/index.js",
"test": "PINO_LOG_LEVEL=silent PORT=5600 vitest --config vitest.config.ts",
Expand Down
61 changes: 48 additions & 13 deletions src/api/v2/queries/history.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import type { Request, Response } from "express";
import { CACHE_TTL_ANCHORED, CACHE_TTL_PENDING, DPID_ENV, getCeramicClient } from "../../../util/config.js";
import { type CeramicClient } from "@desci-labs/desci-codex-lib";
import parentLogger from "../../../logger.js";
import { resolveDpid } from "../resolvers/dpid.js";
import parentLogger, { serializeError } from "../../../logger.js";
import { DpidResolverError, resolveDpid } from "../resolvers/dpid.js";
import { isDpid } from "../../../util/validation.js";
import { CommitID, StreamID } from "@desci-labs/desci-codex-lib/dist/streams.js";
import { getFromCache, keyBump, setToCache } from "../../../redis.js";
import { cleanupEip155Address } from "../../../util/conversions.js";

const MODULE_PATH = "api/v2/queries/history" as const;
const logger = parentLogger.child({
module: "api/v2/queries/history",
module: MODULE_PATH,
});

export type HistoryQueryRequest = {
Expand All @@ -22,7 +23,15 @@ export type HistoryQueryParams = {
id?: string;
};

export type HistoryQueryResponse = HistoryQueryResult[] | HistoryQueryError;
export type ErrorResponse = {
error: string;
details: unknown;
body: unknown;
params: unknown;
path: typeof MODULE_PATH;
};

export type HistoryQueryResponse = HistoryQueryResult[] | ErrorResponse;

export type HistoryVersion = {
/** Manifest CID at this version */
Expand All @@ -44,8 +53,6 @@ export type HistoryQueryResult = {
versions: HistoryVersion[];
};

export type HistoryQueryError = string;

/**
* For one or more IDs, fetch metadata and version history.
* An ID can be both a streamID and a dPID, but a dPID lookup is a bit slower.
Expand All @@ -57,10 +64,20 @@ export const historyQueryHandler = async (
const { id } = req.params;
const { ids = [] } = req.body;

const baseError = {
params: req.params,
body: req.body,
path: MODULE_PATH,
};

if (!Array.isArray(ids)) {
// Received ids in body, but not as array
logger.error({ body: req.body, params: req.params }, "received malformed IDs");
return res.status(400).send("body.ids expects string[]");
logger.error(baseError, "received malformed IDs");
return res.status(400).send({
error: "invalid request",
details: "body.ids expects string[]",
...baseError,
});
}

if (id) {
Expand All @@ -70,8 +87,12 @@ export const historyQueryHandler = async (

if (ids.length === 0) {
// Neither ID format was supplied
logger.error({ body: req.body, params: req.params }, "request missing IDs");
return res.status(400).send("missing /:id or ids array in body");
logger.error(baseError, "request missing IDs");
return res.status(400).send({
error: "invalid request",
details: "missing /:id or ids array in body",
...baseError,
});
}

logger.info({ ids }, "handling history query");
Expand All @@ -87,9 +108,23 @@ export const historyQueryHandler = async (
]);
const result = [...codexHistories, ...dpidHistories];
return res.send(result);
} catch (error) {
logger.error({ ids, error }, "failed to compile histories");
return res.status(500).send("failed to compile histories");
} catch (e) {
if (e instanceof DpidResolverError) {
const errPayload = {
error: "failed to resolve dpid",
details: serializeError(e),
...baseError,
};
logger.error(errPayload, "failed to resolve dpid");
return res.status(500).send(errPayload);
}
const errPayload = {
error: "failed to compile histories",
details: serializeError(e as Error),
...baseError,
};
logger.error(errPayload, "failed to compile histories");
return res.status(500).send(errPayload);
}
};

Expand Down
6 changes: 3 additions & 3 deletions src/api/v2/resolvers/codex.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Request, Response } from "express";
import parentLogger from "../../../logger.js";
import parentLogger, { serializeError } from "../../../logger.js";
import { pidFromStringID, type PID } from "@desci-labs/desci-codex-lib";
import { getCodexHistory, type HistoryQueryResult } from "../queries/history.js";

Expand Down Expand Up @@ -46,7 +46,7 @@ export const resolveCodexHandler = async (
} catch (e) {
const errPayload = {
error: "Invalid stream or commit ID",
details: "Could not coerce ID into neither stream nor commitID",
details: serializeError(e as Error),
params: req.params,
path: MODULE_PATH,
};
Expand All @@ -67,7 +67,7 @@ export const resolveCodexHandler = async (
logger.error({ streamId, versionIx, err }, "failed to resolve stream");
return res.status(404).send({
error: "Could not resolve; does stream/version exist?",
details: err,
details: serializeError(err),
params: req.params,
path: MODULE_PATH,
});
Expand Down
46 changes: 36 additions & 10 deletions src/api/v2/resolvers/dpid.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { Request, Response } from "express";
import parentLogger from "../../../logger.js";
import parentLogger, { serializeError } from "../../../logger.js";
import { CACHE_TTL_ANCHORED, CACHE_TTL_PENDING, DPID_ENV, getDpidAliasRegistry } from "../../../util/config.js";
import { ResolverError } from "../../../errors.js";
import { getCodexHistory, type HistoryQueryResult, type HistoryVersion } from "../queries/history.js";
import { BigNumber } from "ethers";
import { getFromCache, setToCache } from "../../../redis.js";
import type { DpidAliasRegistry } from "@desci-labs/desci-contracts/dist/typechain-types/DpidAliasRegistry.js";
import { BigNumber } from "ethers";

const MODULE_PATH = "/api/v2/resolvers/codex" as const;
const MODULE_PATH = "/api/v2/resolvers/dpid" as const;
const logger = parentLogger.child({
module: MODULE_PATH,
});
Expand Down Expand Up @@ -53,7 +53,7 @@ export const resolveDpidHandler = async (
if (e instanceof DpidResolverError) {
const errPayload = {
error: e.message,
details: e.cause,
details: serializeError(e.cause),
params: req.params,
path: MODULE_PATH,
};
Expand All @@ -63,7 +63,7 @@ export const resolveDpidHandler = async (
const err = e as Error;
const errPayload = {
error: err.message,
details: err,
details: serializeError(err),
params: req.params,
path: MODULE_PATH,
};
Expand Down Expand Up @@ -148,11 +148,15 @@ export const resolveDpid = async (dpid: number, versionIx?: number): Promise<His
setToCache(streamCacheKey, "", CACHE_TTL_PENDING);
}
} else {
// The BigNumbers are parsed into objects, which ethers.BigNumber in fine with
resolvedEntry = JSON.parse(fromCache);
// The BigNumbers are deserialized into objects, which ethers.BigNumber can instantiate from
resolvedEntry[1].forEach((v) => {
v[1] = BigNumber.from(v[1]);
});
}

const [owner, versions] = resolvedEntry;
const owner = resolvedEntry[0];
const versions = undupeIfLegacyDevHistory(resolvedEntry[1]);
const requestedVersion = versions[versionIx ?? versions.length - 1];

result = {
Expand All @@ -163,11 +167,13 @@ export const resolveDpid = async (dpid: number, versionIx?: number): Promise<His
versions: versions.map(([manifest, time]) => ({
// No CommitID available
version: "",
time: BigNumber.from(time).toNumber(),
// When restored from redis, the BigNumber is deserialised as a regular object
// Ethers can instantiate the class from that format
time: time.toNumber(),
manifest,
})),
};
logger.info(result, "manifest resolved via fallback to legacy entry");
logger.info({ dpid, owner, manifest: result.manifest }, "manifest resolved via fallback to legacy entry");
return result;
} catch (e) {
throw new DpidResolverError({
Expand All @@ -179,5 +185,25 @@ export const resolveDpid = async (dpid: number, versionIx?: number): Promise<His
};

type DpidErrorName = "RegistryContactFailed" | "CeramicContactFailed" | "LegacyLookupError" | "DpidNotFound";

export class DpidResolverError extends ResolverError<DpidErrorName> {}

type LegacyVersion = DpidAliasRegistry.LegacyVersionStructOutput;

const undupeIfLegacyDevHistory = (versions: LegacyVersion[]) => {
if (DPID_ENV !== "dev") {
return versions;
}

return versions.reduce((unduped, current) => {
if (unduped.length === 0 || !isLegacyDupe(current, unduped[unduped.length - 1])) {
unduped.push(current);
}
return unduped;
}, [] as LegacyVersion[]);
};

const isLegacyDupe = ([aCid, aTimeBn]: LegacyVersion, [bCid, bTimeBn]: LegacyVersion): boolean => {
const cidIsEqual = aCid === bCid;
const timeIsEqual = aTimeBn.toNumber() === bTimeBn.toNumber();
return cidIsEqual && timeIsEqual;
};
8 changes: 4 additions & 4 deletions src/api/v2/resolvers/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Request, Response } from "express";
import axios from "axios";
import type { ResearchObjectV1 } from "@desci-labs/desci-models";

import parentLogger from "../../../logger.js";
import parentLogger, { serializeError } from "../../../logger.js";
import analytics, { LogEventType } from "../../../analytics.js";
import { IPFS_GATEWAY, getNodesUrl } from "../../../util/config.js";
import { DpidResolverError, resolveDpid } from "./dpid.js";
Expand Down Expand Up @@ -152,7 +152,7 @@ export const resolveGenericHandler = async (
if (e instanceof DpidResolverError) {
const errPayload = {
error: e.message,
details: e.cause,
details: serializeError(e.cause),
...baseError,
};
logger.error(errPayload, "failed to resolve dpid");
Expand All @@ -161,7 +161,7 @@ export const resolveGenericHandler = async (
const err = e as Error;
const errPayload = {
error: err.message,
details: err,
details: serializeError(err),
...baseError,
};
logger.error(errPayload, "unexpected error occurred");
Expand Down Expand Up @@ -216,7 +216,7 @@ export const resolveGenericHandler = async (
// Doesn't seem it was a validDagUrl
const errPayload = {
error: "Failed to resolve DAG URL; check path and versioning",
details: e,
details: serializeError(e as Error),
...baseError,
};
logger.error(errPayload, "got invalid DAG URL");
Expand Down
2 changes: 2 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ function omitBuffer(array: any) {
});
}

export const serializeError = (e: Error) => pino.stdSerializers.err(e);

process.on("uncaughtException", (err) => {
logger.fatal(err, "uncaught exception");
});
3 changes: 2 additions & 1 deletion test/v2/resolvers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ describe("dPID", { timeout: 10_000 }, function () {
.expect(200)
.expect((res) =>
expect(res.body.manifest).toEqual(
"bafkreih5koqw5nvxucidlihwfslknj674oeuroclit74rkaqpe4mq6xuka",
// fourth published CID
"bafkreibn3jhdlsdsonv25t7i2bwtrbkl3jzwjbnnwylpeih3jmmzdhsfmi",
),
);
});
Expand Down
Loading