Skip to content

Commit

Permalink
Fix typing issues from tsc
Browse files Browse the repository at this point in the history
Leveraging the schemas defined using `zod` we can infer the type of most
of the input / output types for the API. There's a pending issue
concerning the type inference of `query` parameters that is stuck to
`never` currently (see honojs/middleware#200).
  • Loading branch information
0237h committed Oct 15, 2023
1 parent bcd672f commit bd9330c
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 40 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
],
"scripts": {
"dev": "bun run --watch src/index.ts",
"lint": "bun run tsc --noEmit --skipLibCheck",
"lint": "bun run tsc --noEmit --skipLibCheck --pretty",
"test": "bun test src/tests/*.spec.ts --coverage"
},
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/banner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import pkg from "../package.json" assert { type: "json" };
import pkg from "../package.json";

// https://fsymbols.com/generators/carty/
export function banner() {
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "dotenv/config";

// TODO: Move to Zod
const EnvSchema = Type.Object({
NODE_ENV: Type.String(),
PORT: Type.String(),
DB_HOST: Type.String(),
DB_NAME: Type.String(),
Expand Down
68 changes: 50 additions & 18 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { OpenAPIHono } from '@hono/zod-openapi';
import { TypedResponse } from 'hono';
import { serveStatic } from 'hono/bun'
import { HTTPException } from 'hono/http-exception';
import { logger } from 'hono/logger';

import pkg from "../package.json" assert { type: "json" };
import pkg from "../package.json";
import * as routes from './routes';
import {
type BlockchainSchema, type BlocknumSchema, type TimestampSchema,
type BlocktimeQueryResponseSchema, type SingleBlocknumQueryResponseSchema, type SupportedChainsQueryResponseSchema
} from './schemas';
import config from "./config";
import { banner } from "./banner";
import { supportedChainsQuery, timestampQuery, blocknumQuery, currentBlocknumQuery, finalBlocknumQuery } from "./queries";
Expand Down Expand Up @@ -36,52 +41,79 @@ app.onError((err, c) => {
return c.json({ error_message }, error_code);
});

app.openapi(routes.indexRoute, (c) => c.text(banner()));
app.openapi(routes.indexRoute, (c) => {
return {
response: c.text(banner())
} as TypedResponse<string>;
});

app.openapi(routes.healthCheckRoute, async (c) => {
type DBStatusResponse = {
db_status: string,
db_response_time_ms: number
};

const start = performance.now();
const dbStatus = await fetch(`${config.DB_HOST}/ping`).then(async (r) => {
return Response.json({
db_status: await r.text(),
db_response_time_ms: performance.now() - start
}, r);
} as DBStatusResponse, r);
}).catch((error) => {
return Response.json({
db_status: error.code,
db_response_time_ms: performance.now() - start
}, { status: 503 });
} as DBStatusResponse, { status: 503 });
});

c.status(dbStatus.status);
return c.json(await dbStatus.json());
return {
response: c.json(await dbStatus.json())
} as TypedResponse<DBStatusResponse>;
});

app.openapi(routes.supportedChainsRoute, async (c) => c.json({ supportedChains: await supportedChainsQuery() }));
app.openapi(routes.supportedChainsRoute, async (c) => {
return {
response: c.json({ supportedChains: await supportedChainsQuery() })
} as TypedResponse<SupportedChainsQueryResponseSchema>;
});

app.openapi(routes.timestampQueryRoute, async (c) => {
const { chain } = c.req.valid('param');
const { block_number } = c.req.valid('query');

return c.json(await timestampQuery(chain, block_number));
// @ts-expect-error: Suppress type of parameter expected to be never (see https://github.com/honojs/middleware/issues/200)
const { chain } = c.req.valid('param') as BlockchainSchema;
// @ts-expect-error: Suppress type of parameter expected to be never (see https://github.com/honojs/middleware/issues/200)
const { block_number } = c.req.valid('query') as BlocknumSchema;

return {
response: c.json(await timestampQuery(chain, block_number))
} as TypedResponse<BlocktimeQueryResponseSchema>;
});

app.openapi(routes.blocknumQueryRoute, async (c) => {
const { chain } = c.req.valid('param');
const { timestamp } = c.req.valid('query');

return c.json(await blocknumQuery(chain, timestamp));
// @ts-expect-error: Suppress type of parameter expected to be never (see https://github.com/honojs/middleware/issues/200)
const { chain } = c.req.valid('param') as BlockchainSchema;
// @ts-expect-error: Suppress type of parameter expected to be never (see https://github.com/honojs/middleware/issues/200)
const { timestamp } = c.req.valid('query') as TimestampSchema;

return {
response: c.json(await blocknumQuery(chain, timestamp))
} as TypedResponse<BlocktimeQueryResponseSchema>;
});

app.openapi(routes.currentBlocknumQueryRoute, async (c) => {
const { chain } = c.req.valid('param');
const { chain } = c.req.valid('param') as BlockchainSchema;

return c.json(await currentBlocknumQuery(chain));
return {
response: c.json(await currentBlocknumQuery(chain))
} as TypedResponse<SingleBlocknumQueryResponseSchema>;
});

app.openapi(routes.finalBlocknumQueryRoute, async (c) => {
const { chain } = c.req.valid('param');
const { chain } = c.req.valid('param') as BlockchainSchema;

return c.json(await finalBlocknumQuery(chain));
return {
response: c.json(await finalBlocknumQuery(chain))
} as TypedResponse<SingleBlocknumQueryResponseSchema>;
});

export default app;
13 changes: 10 additions & 3 deletions src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const BlockchainSchema = z.object({
example: 'EOS',
})
});
export type BlockchainSchema = z.infer<typeof BlockchainSchema>;

export const BlocknumSchema = z.object({
block_number: z_blocknum.openapi({
Expand All @@ -26,28 +27,34 @@ export const BlocknumSchema = z.object({
example: 1337
})
});
export type BlocknumSchema = z.infer<typeof BlocknumSchema>;

export const TimestampSchema = z.object({
timestamp: z_timestamp.openapi({
param: {
name: 'timestamp',
in: 'query',
},
example: new Date()
example: new Date().toISOString()
})
});
export type TimestampSchema = z.infer<typeof TimestampSchema>;

// TODO: Add support for array of response (to mirror array of params)
export const BlocktimeQueryResponseSchema = z.object({
chain: z.enum(supportedChains).openapi({ example: 'EOS' }),
block_number: z_blocknum.optional().openapi({ example: 1337 }),
timestamp: z_timestamp.optional().openapi({ example: new Date() }),
timestamp: z_timestamp.optional().openapi({ example: new Date().toISOString() }),
}).openapi('BlocktimeQuery');
export type BlocktimeQueryResponseSchema = z.infer<typeof BlocktimeQueryResponseSchema>;

export const SingleBlocknumQueryResponseSchema = z.object({
chain: z.enum(supportedChains).openapi({ example: 'EOS' }),
block_number: z_blocknum.optional().openapi({ example: 1337 }),
}).openapi('SingleBlocknumQuery');
export type SingleBlocknumQueryResponseSchema = z.infer<typeof SingleBlocknumQueryResponseSchema>;

export const SupportedChainsQueryResponseSchema = z.object({
supportedChains: z.enum(supportedChains).array().openapi({ example: supportedChains })
}).openapi('SupportedChainsQuery');
}).openapi('SupportedChainsQuery');
export type SupportedChainsQueryResponseSchema = z.infer<typeof SupportedChainsQueryResponseSchema>;
2 changes: 1 addition & 1 deletion src/tests/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('Config from .env', () => {
});

it('Should not load .env variables with wrong types', () => {
process.env.PORT = parseInt(process.env.port);
process.env.PORT = process.env.port;

expect(() => decode()).toThrow();
});
Expand Down
41 changes: 28 additions & 13 deletions src/tests/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { describe, expect, it, beforeAll } from 'bun:test';
import { ZodError } from 'zod';

import app from '../index';
import config from '../config';
import { banner } from "../banner";
import { supportedChainsQuery, timestampQuery } from "../queries";
import { BlocktimeQueryResponseSchema, SingleBlocknumQueryResponseSchema} from '../schemas';
import {
BlockchainSchema, BlocknumSchema, TimestampSchema,
BlocktimeQueryResponseSchema, SingleBlocknumQueryResponseSchema, SupportedChainsQueryResponseSchema
} from '../schemas';

const supportedChains = await supportedChainsQuery();

Expand All @@ -28,10 +32,9 @@ describe('Chains page (/chains)', () => {

it('Should return the supported chains as JSON', async () => {
const res = await app.request('/chains');
const json = await res.json();
const json = await res.json() as unknown;

expect(json).toHaveProperty('supportedChains');
expect(json.supportedChains).toEqual(supportedChains);
expect(BlocktimeQueryResponseSchema.safeParse(json).success).toBe(true);
});
});

Expand All @@ -47,12 +50,18 @@ describe('Health page (/health)', () => {
});

describe('Timestamp query page (/{chain}/timestamp?block_number=<block number>)', () => {
let valid_chain;
let valid_blocknum;
let valid_chain: any;
let valid_blocknum: any;

beforeAll(() => {
valid_chain = supportedChains[0];
valid_blocknum = 1337;
valid_chain = BlockchainSchema.safeParse(supportedChains[0]);
valid_blocknum = BlocknumSchema.safeParse(1337);

expect(valid_chain.success).toBe(true);
expect(valid_blocknum.success).toBe(true);

valid_chain = valid_chain.data;
valid_blocknum = valid_blocknum.data;
});

it('Should fail on non-valid chains', async () => {
Expand Down Expand Up @@ -92,12 +101,18 @@ describe('Timestamp query page (/{chain}/timestamp?block_number=<block number>)'
});

describe('Blocknum query page (/{chain}/blocknum?timestamp=<timestamp>)', () => {
let valid_chain;
let valid_timestamp;
let valid_chain: any;
let valid_timestamp: any;

beforeAll(() => {
valid_chain = supportedChains[0];
valid_timestamp = new Date();
valid_chain = BlockchainSchema.safeParse(supportedChains[0]);
valid_timestamp = BlocknumSchema.safeParse(new Date());

expect(valid_chain.success).toBe(true);
expect(valid_timestamp.success).toBe(true);

valid_chain = valid_chain.data;
valid_timestamp = valid_timestamp.data;
});

it('Should fail on non-valid chains', async () => {
Expand All @@ -113,7 +128,7 @@ describe('Blocknum query page (/{chain}/blocknum?timestamp=<timestamp>)', () =>
const res = await app.request(`/${valid_chain}/blocknum?timestamp=${timestamp}`);
expect(res.status).toBe(400);

const json = await res.json();
const json = await res.json() as ZodError;
expect(json.success).toBe(false);
expect(json.error.issues[0].code).toBe('invalid_date');
});
Expand Down
37 changes: 34 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,38 @@
{
"compilerOptions": {
"esModuleInterop": true,
// add Bun type definitions
"types": ["bun-types"],

// enable latest features
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",

// if TS 5.x+
"moduleResolution": "bundler",
"noEmit": true,
"allowImportingTsExtensions": true,
"moduleDetection": "force",
// if TS 4.x or earlier
// "moduleResolution": "nodenext",

"jsx": "react-jsx", // support JSX
"allowJs": true, // allow importing `.js` from `.ts`

// best practices
"strict": true,
"types": ["bun-types"]
}
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"composite": true,
"downlevelIteration": true,
"allowSyntheticDefaultImports": true
},
"include": [
"src/**/*.ts",
"package.json"
],
"exclude": [
"node_modules",
"src/tests/*.spec.ts"
]
}

0 comments on commit bd9330c

Please sign in to comment.