Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/fair-ghosts-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensapi": patch
---

Fixes error handling in app.onError to return correct HTTP status codes and resolves OpenAPI schema generation issue
5 changes: 5 additions & 0 deletions .changeset/nice-foxes-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensapi": patch
---

use example requests for openapi doc
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
use example requests for openapi doc
add example responses to autogenerated openapi doc

These are example responses, right?

5 changes: 5 additions & 0 deletions .changeset/smooth-foxes-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/ensnode-sdk": patch
---

add examples for type system
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as a reader i'm not sure what this is referring to

2 changes: 1 addition & 1 deletion apps/ensapi/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ app.get("/health", async (c) => {
// log hono errors to console
app.onError((error, ctx) => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this fallback catch-all error handler, what do you think about adding a prefix to the log messages / `errorResponse that would clearly identify for us that the error went through this fallback catch-all error handler.

Goal: Help us with debugging.

For example, maybe a little prefix such as "Unexpected server error: ..."?

Appreciate your advice.

logger.error(error);
return errorResponse(ctx, "Internal Server Error");
return errorResponse(ctx, error);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the wrong approach: this would catch any uncaught or unexpected errors and leak internal error messages to the public.

instead, the handlers should catch errors and return an error response, and this function should never be executed. by definition if this fn is executed, there's some unexpected internal server error, and that's what we should return

});

export default app;
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ErrorResponseSchema,
makeNameTokensResponseSchema,
makeNodeSchema,
nameTokensResponseOkExample,
} from "@ensnode/ensnode-sdk/internal";

import { params } from "@/lib/handlers/params.schema";
Expand Down Expand Up @@ -42,7 +43,9 @@ export const getNameTokensRoute = createRoute({
description: "Name tokens known",
content: {
"application/json": {
schema: makeNameTokensResponseSchema("Name Tokens Response", true),
schema: makeNameTokensResponseSchema("Name Tokens Response", true).openapi({
example: nameTokensResponseOkExample,
}),
},
},
},
Expand Down Expand Up @@ -81,5 +84,3 @@ export const getNameTokensRoute = createRoute({
},
},
});

export const routes = [getNameTokensRoute];
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import {
RegistrarActionsOrders,
} from "@ensnode/ensnode-sdk";
import {
ErrorResponseSchema,
makeLowercaseAddressSchema,
makeNodeSchema,
makePositiveIntegerSchema,
makeRegistrarActionsResponseErrorSchema,
makeSerializedRegistrarActionsResponseOkSchema,
makeUnixTimestampSchema,
registrarActionsResponseOkExample,
} from "@ensnode/ensnode-sdk/internal";

import { params } from "@/lib/handlers/params.schema";
Expand Down Expand Up @@ -59,12 +63,14 @@ export const registrarActionsQuerySchema = z
.pipe(z.coerce.number())
.pipe(makeUnixTimestampSchema("beginTimestamp"))
.optional()
.openapi({ type: "integer" })
.describe("Filter actions at or after this Unix timestamp"),

endTimestamp: params.queryParam
.pipe(z.coerce.number())
.pipe(makeUnixTimestampSchema("endTimestamp"))
.optional()
.openapi({ type: "integer" })
.describe("Filter actions at or before this Unix timestamp"),
})
.refine(
Expand Down Expand Up @@ -96,12 +102,27 @@ export const getRegistrarActionsRoute = createRoute({
responses: {
200: {
description: "Successfully retrieved registrar actions",
content: {
"application/json": {
schema: makeSerializedRegistrarActionsResponseOkSchema(
"Registrar Actions Response",
).openapi({
example: registrarActionsResponseOkExample,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be awesome if you could make it so that we could produce multiple examples in the OpenAPI spec. For example, both an Ok and and Error example.

It's important that we document examples for error cases too.

For some of our APIs, it would also be valuable if we could document various edge cases for success responses too.

What do you think? Please feel welcome to create a separate follow-up issue and PR for this goal.

}),
},
},
},
400: {
description: "Invalid query",
content: { "application/json": { schema: ErrorResponseSchema } },
},
500: {
description: "Internal server error",
content: {
"application/json": {
schema: makeRegistrarActionsResponseErrorSchema("Registrar Actions Error Response"),
},
},
},
},
});
Expand All @@ -125,14 +146,29 @@ export const getRegistrarActionsByParentNodeRoute = createRoute({
responses: {
200: {
description: "Successfully retrieved registrar actions",
content: {
"application/json": {
schema: makeSerializedRegistrarActionsResponseOkSchema(
"Registrar Actions By ParentNode Response",
).openapi({
example: registrarActionsResponseOkExample,
}),
},
},
},
400: {
description: "Invalid input",
content: { "application/json": { schema: ErrorResponseSchema } },
},
500: {
description: "Internal server error",
content: {
"application/json": {
schema: makeRegistrarActionsResponseErrorSchema(
"Registrar Actions By ParentNode Error Response",
),
},
},
},
},
});

export const routes = [getRegistrarActionsRoute, getRegistrarActionsByParentNodeRoute];
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ app.openapi(getRegistrarActionsRoute, async (c) => {
registrarActions,
pageContext,
} satisfies RegistrarActionsResponseOk),
200,
);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
Expand Down Expand Up @@ -166,6 +167,7 @@ app.openapi(getRegistrarActionsByParentNodeRoute, async (c) => {
pageContext,
accurateAsOf,
} satisfies RegistrarActionsResponseOk),
200,
);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
Expand Down
2 changes: 0 additions & 2 deletions apps/ensapi/src/handlers/api/meta/realtime-api.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,3 @@ export const realtimeGetMeta = createRoute({
},
},
});

export const routes = [realtimeGetMeta];
2 changes: 0 additions & 2 deletions apps/ensapi/src/handlers/api/meta/status-api.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,3 @@ export const getIndexingStatusRoute = createRoute({
},
},
});

export const routes = [getConfigRoute, getIndexingStatusRoute];
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import {
makeResolvePrimaryNameResponseSchema,
makeResolvePrimaryNamesResponseSchema,
makeResolveRecordsResponseSchema,
resolvePrimaryNameResponseExample,
resolvePrimaryNamesResponseExample,
resolveRecordsResponseExample,
} from "@ensnode/ensnode-sdk/internal";

import { params } from "@/lib/handlers/params.schema";
Expand Down Expand Up @@ -38,7 +41,9 @@ export const resolveRecordsRoute = createRoute({
description: "Successfully resolved records",
content: {
"application/json": {
schema: makeResolveRecordsResponseSchema(),
schema: makeResolveRecordsResponseSchema().openapi({
example: resolveRecordsResponseExample,
}),
},
},
},
Expand Down Expand Up @@ -67,7 +72,9 @@ export const resolvePrimaryNameRoute = createRoute({
description: "Successfully resolved name",
content: {
"application/json": {
schema: makeResolvePrimaryNameResponseSchema(),
schema: makeResolvePrimaryNameResponseSchema().openapi({
example: resolvePrimaryNameResponseExample,
}),
},
},
},
Expand Down Expand Up @@ -96,11 +103,11 @@ export const resolvePrimaryNamesRoute = createRoute({
description: "Successfully resolved records",
content: {
"application/json": {
schema: makeResolvePrimaryNamesResponseSchema(),
schema: makeResolvePrimaryNamesResponseSchema().openapi({
example: resolvePrimaryNamesResponseExample,
}),
},
},
},
},
});

export const routes = [resolveRecordsRoute, resolvePrimaryNameRoute, resolvePrimaryNamesRoute];
2 changes: 1 addition & 1 deletion apps/ensapi/src/lib/handlers/params.schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe("params.selection", () => {
it("parses selection", () => {
expect(
params.selection.parse({
name: "true",
nameRecord: "true",
addresses: "60,0",
texts: "example,hello",
}),
Expand Down
60 changes: 47 additions & 13 deletions apps/ensapi/src/lib/handlers/params.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,33 +37,67 @@ const stringarray = z
const name = z
.string()
.refine(isNormalizedName, "Must be normalized, see https://docs.ens.domains/resolution/names/")
.transform((val) => val as Name);
.transform((val) => val as Name)
.describe("ENS name to resolve (e.g. 'vitalik.eth'). Must be normalized per ENSIP-15.");

const trace = z.optional(boolstring).default(false).openapi({ default: false });
const accelerate = z.optional(boolstring).default(false).openapi({ default: false });
const address = makeLowercaseAddressSchema();
const defaultableChainId = makeDefaultableChainIdStringSchema();
const coinType = makeCoinTypeStringSchema();
const trace = z
.optional(boolstring)
.default(false)
.describe("Include detailed resolution trace information in the response.")
.openapi({ default: false });

const chainIdsWithoutDefaultChainId = z.optional(
stringarray.pipe(z.array(defaultableChainId.pipe(excludingDefaultChainId))),
const accelerate = z
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const accelerate = z
const attemptAcceleration = z

I believe we should rename this param as suggested above. This is because accelerate sounds like a deterministic command. Ex: I asked to accelerate, therefore the system must always accelerate. However, if our API builds this narrative it will create incorrect mental models in people who integrate with us.

We need to soften the language. Ex: attemptAcceleration so that it creates the more accurate mental model that acceleration will be attempted but is not guaranteed. A number of factors determine if a resolution request can be accelerated or not. Some of these factors are unlikely to be known by clients at the time they submit a resolution request. We don't want to put this burden on clients building on our APIs. Instead they ask to resolve and we always do whatever is necessary to guarantee we return a correct response. The implementation may or may not be accelerated -- the actual implementation of serving a resolution request should be controlled by the server.

.optional(boolstring)
.default(false)
.describe("Attempt accelerated CCIP-Read resolution using L1 data.")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our acceleration is not constrained only to L1 data. We can fully accelerate resolutions for some "L2 names" such as jesse.base.eth

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah maybe just Attempt Protocol Acceleration using indexed data

.openapi({
default: false,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we should update this to accelerate by default. Please feel welcome to create a separate follow-up issue / PR for this.

});
const address = makeLowercaseAddressSchema().describe(
"EVM wallet address (e.g. '0xd8da6bf26964af9d7eed9e03e53415d37aa96045').",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we note in this description that it must be all lowercase?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i believe we accept checksummed addresses and the lowercase schema normalizes them

);
const defaultableChainId = makeDefaultableChainIdStringSchema().describe(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I note how these describe calls are being added in this PR to help make the OpenAPI specs better. That's great!

I'm curious though about putting these describe calls in this file? I assume it would be better to add these describe calls into the existing Zod schemas we define in ensnode-sdk?

In other words, this describe metadata should apply anywhere we use something like a makeDefaultableChainIdStringSchema. Not just here. Right?

"Chain ID as a string (e.g. '1' for Ethereum mainnet). Use '0' for the default EVM chain.",
);
const coinType = makeCoinTypeStringSchema();

const chainIdsWithoutDefaultChainId = z
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume we should invert the strategy used here?

Here, I see we are starting with defaultable and then removing the default chain id case.

Why don't we start with the regular (non-defaultable) chainid case and then add the special defaultable case? That mirrors the approach taken here:

export type DefaultableChainId = 0 | ChainId;

Additionally, the idea of a "defaultable chain id" only exists within the context of ENS resolution, while the idea of a chain id exists in many other contexts. Therefore I don't think we should include ideas such as "... The default EVM chain ID (0) is not allowed." when describing a regular (non-defaultable) chainid field.

.optional(stringarray.pipe(z.array(defaultableChainId.pipe(excludingDefaultChainId))))
.describe(
"Comma-separated list of chain IDs to resolve primary names for (e.g. '1,10,8453'). The default EVM chain ID (0) is not allowed.",
);

const rawSelectionParams = z.object({
name: z.string().optional(),
addresses: z.string().optional(),
texts: z.string().optional(),
nameRecord: z
.string()
.optional()
.describe("Whether to include the ENS name record in the response.")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see my other comments. We should significantly refine the way we are describing this field.

.openapi({
enum: ["true", "false"],
}),
addresses: z
Comment on lines 70 to +78
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 OpenAPI type: "boolean" on a z.string() field

rawSelectionParams.nameRecord is declared as z.string() but then annotated with .openapi({ type: "boolean" }). This tells OpenAPI code generators and consumers that the parameter is a JSON boolean, yet over HTTP it is still a plain query-string value ("true" / "false"). Some clients may therefore attempt to serialise it as a literal true (no quotes), which is invalid for a query param.

A more accurate OpenAPI annotation would be:

Suggested change
const rawSelectionParams = z.object({
name: z.string().optional(),
addresses: z.string().optional(),
texts: z.string().optional(),
nameRecord: z
.string()
.optional()
.openapi({
type: "boolean",
description: "Whether to include the ENS name record in the response.",
}),
addresses: z
nameRecord: z
.string()
.optional()
.openapi({
type: "string",
enum: ["true", "false"],
description: "Whether to include the ENS name record in the response.",
}),

Alternatively, simply reuse boolstring (which already carries the correct .openapi({ type: "boolean" }) hint that @hono/zod-openapi understands from a string-with-coercion context) to stay consistent with the addresses / texts siblings.

.string()
.optional()
.describe(
"Comma-separated list of coin types to resolve addresses for (e.g. '60' for ETH, '2147483658' for OP).",
),
texts: z
.string()
.optional()
.describe(
"Comma-separated list of text record keys to resolve (e.g. 'avatar,description,url').",
),
});

const selection = z
.object({
name: z.optional(boolstring),
nameRecord: z.optional(boolstring),
Copy link
Copy Markdown
Member

@lightwalker-eth lightwalker-eth Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
nameRecord: z.optional(boolstring),
reverseName: z.optional(boolstring),

It will help if we document in appropriate places what this reverseName record relates to.

I'm going to invest some meaningful effort in writing ideas for you here that I hope will be a good resource for you in taking the lead on building the ultimate ENS resolution APIs. There's some meaningful complexity here. It's ok to not 100% master all the details just yet but please take special note of everything I write here for your future work.

Here's the main documentation about reverse resolution:

  1. https://docs.ens.domains/ensip/3
    1. Specifically here: https://docs.ens.domains/ensip/3#resolver-interface
    2. More complex extension of this idea for a multichain context: https://docs.ens.domains/ensip/19
  2. https://docs.ens.domains/web/reverse

In other words:

  • ENS Reverse Resolution (as defined in ENSIP-3) defines a process for converting from an address (implicitly on Ethereum Mainnet) to a primary name for that address.
  • ENSIP-19 extends the ideas of ENSIP-3 to enable:
    • ... an address to set distinct primary names on different chains. For example, address X may be a contract that can only work on chain Y. Therefore it may only set a primary name on chain Y that points back to address X. This can be important as consider if someone sent assets to address X on chain Z -- this might result in those assets being forever lost because address X is a contract tied exclusively to chain Y and not an EOA that works across all chains.
    • ... the concept of a "default" primary name to be used as a fallback primary name if no chain-specific primary name is configured.
    • ... the ENS reverse resolution process (primary name lookup) for looking up the primary name of an address on any chain.
  • The ENSNode resolution APIs should fully support everything for ENSIP-19 already. Suggest to use our inspection tools in ENSAdmin as a resource for understanding these ideas in more detail.

Also highly suggest to add documentation about this reverseName field in appropriate places. This documentation should include:

  1. How we call it reverseName within our APIs to hopefully reduce confusion with how "name" is otherwise overloaded terminology in so many places in ENS.
  2. Technically this field is related to the name resolver record as defined in ENSIP-3 and extended in ENSIP-19.
  3. The name resolver record in ENSIP-3 and ENSIP-19 is exclusively used as an internal implementation detail of the multi-step ENS reverse resolution (primary name lookup) process. The name resolver record returns an unvalidated claim of which name represents the primary name for an address. Note that to be a validated primary name for an address, ENS reverse resolution requires additional bi-directional validation where an unvalidated claim to a primary name is validated through an additional ENS forward resolution step. For details see ENSIP-3, ENSIP-19 and https://docs.ens.domains/web/reverse.

Please note how above I noted how this reverseName record is an internal implementation detail of the multi-step ENS reverse resolution process. For virtually anyone building on ENS who isn't creating specialized tooling, they should never query this directly. Therefore I think it might help to reduce confusion for developers who are new to ENS to not group this field in with the other fields that are 99.99999% of the use cases that devs building on ENS care about.

In other words, maybe we should move this field out of the root of the selection object (at an API layer). I think it would be helpful to only include options at the root of the selection object that 99.99999999% of developers building on ENS care about.

We should still expose the ability to query the reverseName field, but perhaps we hide this capability under a separate query param that's more clearly identified for advanced special cases only.

For example, maybe add nesting for this field in the selection object where it is nested into a new special optional field named something like advancedOptions. This way it still technically exists while it would then be self-documenting how it is for special cases only and that most devs can ignore it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate if you can put a special effort into adding some nice docs about this idea in all the relevant places across our whole monorepo. It's a complex idea and I really want to see us investing strongly in our docs.

This includes docs for this field within the Drizzle schemas that are defined in ENSDb.

addresses: z.optional(stringarray.pipe(z.array(coinType))),
texts: z.optional(stringarray),
})
.transform((value, ctx) => {
const selection: ResolverRecordsSelection = {
...(value.name && { name: true }),
...(value.nameRecord && { name: true }),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
...(value.nameRecord && { name: true }),
...(value.nameRecord && { nameRecord: true }),

Should we be renaming other things too?

For example, I'm surprised this PR is not making updates to our ENS Protocol Acceleration logic inside ENSApi. I would have expected more things to change if we rename this idea.

Appreciate your advice.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo we should leave the protocol acceleration lib as-is — so the current change is acceptable — the handler accepts nameRecord (or whatever we end up calling it) and passes the configuration option to the protocol acceleration lib as name: true. since there's no confusion within that module.

finally, if we're going to rename name to nameRecord i might propose we just rename all of the fields for ultimate clarity like resolveNameRecord, resolveTextRecords and resolveAddressRecords

...(value.addresses && { addresses: value.addresses }),
...(value.texts && { texts: value.texts }),
};
Expand Down
16 changes: 8 additions & 8 deletions apps/ensindexer/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,17 +193,17 @@ describe("config (with base env)", () => {

it("throws an error when ENSINDEXER_SCHEMA_NAME is not set", async () => {
vi.stubEnv("ENSINDEXER_SCHEMA_NAME", undefined);
await expect(getConfig()).rejects.toThrow(/ENSIndexer Schema Name is required/);
await expect(getConfig()).rejects.toThrow(/ENSINDEXER_SCHEMA_NAME is required/);
});

it("throws an error when ENSINDEXER_SCHEMA_NAME is empty", async () => {
vi.stubEnv("ENSINDEXER_SCHEMA_NAME", "");
await expect(getConfig()).rejects.toThrow(/ENSIndexer Schema Name cannot be an empty string/);
await expect(getConfig()).rejects.toThrow(/ENSINDEXER_SCHEMA_NAME cannot be an empty string/);
});

it("throws an error when ENSINDEXER_SCHEMA_NAME is only whitespace", async () => {
vi.stubEnv("ENSINDEXER_SCHEMA_NAME", " ");
await expect(getConfig()).rejects.toThrow(/ENSIndexer Schema Name cannot be an empty string/);
await expect(getConfig()).rejects.toThrow(/ENSINDEXER_SCHEMA_NAME cannot be an empty string/);
});
});

Expand Down Expand Up @@ -423,27 +423,27 @@ describe("config (with base env)", () => {

it("throws an error if ENSDB_URL is not set", async () => {
vi.stubEnv("ENSDB_URL", undefined);
await expect(getConfig()).rejects.toThrow(/Invalid input/);
await expect(getConfig()).rejects.toThrow(/ENSDB_URL is required/);
});

it("throws an error if ENSDB_URL is empty", async () => {
vi.stubEnv("ENSDB_URL", "");
await expect(getConfig()).rejects.toThrow(/Invalid PostgreSQL connection string/);
await expect(getConfig()).rejects.toThrow(/Invalid connection string/);
});

it("throws an error if ENSDB_URL is not a valid postgres connection string", async () => {
vi.stubEnv("ENSDB_URL", "not-a-postgres-connection-string");
await expect(getConfig()).rejects.toThrow(/Invalid PostgreSQL connection string/);
await expect(getConfig()).rejects.toThrow(/Invalid connection string/);
});

it("throws an error if ENSDB_URL uses the wrong protocol", async () => {
vi.stubEnv("ENSDB_URL", "mysql://user:password@localhost:3306/mydb");
await expect(getConfig()).rejects.toThrow(/Invalid PostgreSQL connection string/);
await expect(getConfig()).rejects.toThrow(/Invalid connection string/);
});

it("throws an error if ENSDB_URL is missing required components", async () => {
vi.stubEnv("ENSDB_URL", "postgresql://localhost:5432");
await expect(getConfig()).rejects.toThrow(/Invalid PostgreSQL connection string/);
await expect(getConfig()).rejects.toThrow(/Invalid connection string/);
});

it("accepts postgres:// protocol", async () => {
Expand Down
Loading
Loading