Skip to content

Add environment variable error handling #23

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

Merged
merged 16 commits into from
Feb 24, 2023
Merged
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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
PORT=8080
LOG_LEVEL="info"
CORS_ORIGIN="*"

PG_CONN="postgres://postgres:password@localhost:5432/archive"

ENABLE_GRAPHIQL="true"
ENABLE_INTROSPECTION="true"

ENABLE_LOGGING="true"
ENABLE_JAEGER="true"
JAEGER_SERVICE_NAME="archive-api"
JAEGER_ENDPOINT='http://localhost:14268/api/traces'
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
"url": "https://github.com/o1-labs/Archive-Node-API"
},
"main": "build/src/index.js",
"start": "build/src/index.js",
"scripts": {
"build": "tsc",
"start": "node build/src/index.js",
12 changes: 10 additions & 2 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -4,8 +4,16 @@ export interface GraphQLContext {
db_client: DatabaseAdapter;
}

export function buildContext(connectionString: string | undefined) {
export async function buildContext(connectionString: string | undefined) {
const db_client = new ArchiveNodeAdapter(connectionString);
try {
await db_client.checkSQLSchema();
} catch (e) {
throw new Error(
`Could not connect to Postgres with the specified connection string. Please check that Postgres is available and that your connection string is correct and try again.\nReason: ${e}`
);
}
return {
db_client: new ArchiveNodeAdapter(connectionString),
db_client,
};
}
32 changes: 27 additions & 5 deletions src/db/archive-node-adapter/index.ts
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import {
Action,
Actions,
BlockStatusFilter,
defaultTokenID,
DEFAULT_TOKEN_ID,
Event,
Events,
} from '../../models/types';
@@ -13,7 +13,12 @@ import {
createEvent,
createAction,
} from '../../models/utils';
import { getActionsQuery, getEventsQuery } from './queries';
import {
getActionsQuery,
getEventsQuery,
getTables,
USED_TABLES,
} from './queries';

import type { DatabaseAdapter } from '../index';
import type { EventFilterOptionsInput } from '../../resolvers-types';
@@ -22,10 +27,27 @@ export class ArchiveNodeAdapter implements DatabaseAdapter {
private client: postgres.Sql;

constructor(connectionString: string | undefined) {
if (!connectionString) throw new Error('Missing connection string');
if (!connectionString)
throw new Error(
'Missing Postgres Connection String. Please provide a valid connection string in the environment variables or in your configuration file to connect to the Postgres database.'
);
this.client = postgres(connectionString);
}

async checkSQLSchema() {
const tables = await (
await getTables(this.client)
).map((table) => table.tablename);

for (const table of USED_TABLES) {
if (!tables.includes(table)) {
throw new Error(
`Missing table ${table}. Please make sure the table exists in the database.`
);
}
}
}

async close() {
return this.client.end();
}
@@ -60,7 +82,7 @@ export class ArchiveNodeAdapter implements DatabaseAdapter {
const { address, to, from } = input;
let { tokenId, status } = input;

tokenId ||= defaultTokenID;
tokenId ||= DEFAULT_TOKEN_ID;
status ||= BlockStatusFilter.all;
if (to && from && to < from) {
throw new Error('to must be greater than from');
@@ -80,7 +102,7 @@ export class ArchiveNodeAdapter implements DatabaseAdapter {
const { address, to, from } = input;
let { tokenId, status } = input;

tokenId ||= defaultTokenID;
tokenId ||= DEFAULT_TOKEN_ID;
status ||= BlockStatusFilter.all;
if (to && from && to < from) {
throw new Error('to must be greater than from');
19 changes: 19 additions & 0 deletions src/db/archive-node-adapter/queries.ts
Original file line number Diff line number Diff line change
@@ -160,3 +160,22 @@ export function getActionsQuery(
ORDER BY timestamp DESC, state_hash DESC
`;
}

export function getTables(db_client: postgres.Sql) {
return db_client`
SELECT tablename FROM pg_catalog.pg_tables where schemaname='public';
`;
}

export const USED_TABLES = [
'blocks',
'account_identifiers',
'accounts_accessed',
'blocks_zkapp_commands',
'zkapp_commands',
'zkapp_account_update',
'zkapp_account_update_body',
'zkapp_events',
'zkapp_state_data_array',
'zkapp_state_data',
] as const;
2 changes: 2 additions & 0 deletions src/envionment.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
declare global {
namespace NodeJS {
interface ProcessEnv {
LOG_LEVEL: string;
PORT?: string;
PG_CONN: string;
CORS_ORIGIN?: string;
ENABLE_LOGGING?: bool;
ENABLE_INTROSPECTION?: bool;
ENABLE_GRAPHIQL?: bool;
ENABLE_JAEGER?: bool;
13 changes: 6 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -6,21 +6,20 @@ import { buildServer } from './server';

const PORT = process.env.PORT || 8080;

function main() {
const context = buildContext(process.env.PG_CONN);
(async function main() {
const context = await buildContext(process.env.PG_CONN);
const server = buildServer(context);

['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => {
process.on(signal, () => server.close());
});

server.on('close', () => {
context.db_client.close();
server.on('close', async () => {
await context.db_client.close();
process.exit(1);
});

server.listen(PORT, () => {
console.info(`Server is running on port: ${PORT}`);
});
}

main();
})();
4 changes: 2 additions & 2 deletions src/models/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const defaultTokenID =
'wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf';
export const DEFAULT_TOKEN_ID =
'wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf' as const;

export enum BlockStatusFilter {
all = 'ALL',
2 changes: 1 addition & 1 deletion src/models/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { BlockInfo, TransactionInfo, Event, Action } from './types';
import type postgres from 'postgres';
import type { BlockInfo, TransactionInfo, Event, Action } from './types';

export function createBlockInfo(row: postgres.Row) {
return {
6 changes: 2 additions & 4 deletions src/resolvers.ts
Original file line number Diff line number Diff line change
@@ -5,12 +5,10 @@ import { Resolvers } from './resolvers-types';
export const resolvers: Resolvers = {
Query: {
events: async (_, { input }, { db_client }) => {
const fetchedEvents = await db_client.getEvents(input);
return fetchedEvents;
return db_client.getEvents(input);
},
actions: async (_, { input }, { db_client }) => {
const fetchedActions = await db_client.getActions(input);
return fetchedActions;
return db_client.getActions(input);
},
},
};
100 changes: 89 additions & 11 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,109 @@
import { createYoga, LogLevel } from 'graphql-yoga';
import { createServer } from 'http';
import { useLogger } from '@envelop/core';
import { useGraphQlJit } from '@envelop/graphql-jit';
import { useDisableIntrospection } from '@envelop/disable-introspection';
import { useOpenTelemetry } from '@envelop/opentelemetry';

import { request } from 'node:http';
import { inspect } from 'node:util';

import { buildProvider } from './tracing';
import { schema } from './resolvers';
import type { GraphQLContext } from './context';

const LOG_LEVEL = (process.env.LOG_LEVEL as LogLevel) || 'info';

export function buildServer(context: GraphQLContext) {
function initJaegerProvider() {
let provider = undefined;
if (process.env.ENABLE_JAEGER) {
provider = buildProvider();
if (!process.env.JAEGER_ENDPOINT) {
throw new Error(
'Jaeger was enabled but no endpoint was specified. Please ensure that the Jaeger endpoint is properly configured and available.'
);
}
if (!process.env.JAEGER_SERVICE_NAME) {
throw new Error(
'Jaeger was enabled but no service name was specified. Please ensure that the Jaeger service name is properly configured.'
);
}

// Check if Jaeger endpoint is available.
const endpoint = process.env.JAEGER_ENDPOINT.replace(/(^\w+:|^)\/\//, '');
// eslint-disable-next-line prefer-const
let [hostname, port] = endpoint.split(':');
port = port?.split('/')[0];
const req = request({
hostname,
method: 'GET',
port,
path: '/',
timeout: 2000,
});
req.on('error', () => {
throw new Error(
'Jaeger endpoint not available. Please ensure that the Jaeger endpoint is properly configured and available.'
);
});
req.on('timeout', () => {
throw new Error(
'Jaeger endpoint timed out. Please ensure that the Jaeger endpoint is properly configured and available.'
);
});
req.end();
req.socket?.end?.();
}
return provider;
}

function buildPlugins() {
const plugins = [];

plugins.push(useGraphQlJit());
if (process.env.ENABLE_LOGGING) {
const provider = initJaegerProvider();
plugins.push(
useOpenTelemetry(
{
resolvers: false, // Tracks resolvers calls, and tracks resolvers thrown errors
variables: true, // Includes the operation variables values as part of the metadata collected
result: true, // Includes execution result object as part of the metadata collected
},
provider
)
);
}

if (!process.env.ENABLE_INTROSPECTION)
plugins.push(useDisableIntrospection());

plugins.push(
useOpenTelemetry(
{
resolvers: false, // Tracks resolvers calls, and tracks resolvers thrown errors
variables: true, // Includes the operation variables values as part of the metadata collected
result: true, // Includes execution result object as part of the metadata collected
useLogger({
logFn: (eventName, args) => {
if (args?.result?.errors) {
console.debug(
eventName,
inspect(args.args.contextValue.params, {
showHidden: false,
depth: null,
colors: true,
}),
inspect(args.result.errors, {
showHidden: false,
depth: null,
colors: true,
})
);
}
},
process.env.ENABLE_JAEGER ? buildProvider() : undefined
)
})
);
if (process.env.ENABLE_INTROSPECTION !== 'true')
plugins.push(useDisableIntrospection());
return plugins;
}

export function buildServer(context: GraphQLContext) {
const plugins = buildPlugins();
const yoga = createYoga<GraphQLContext>({
schema,
logging: LOG_LEVEL,
@@ -36,7 +114,7 @@ export function buildServer(context: GraphQLContext) {
plugins,
cors: {
origin: process.env.CORS_ORIGIN ?? '*',
methods: ['GET'],
methods: ['GET', 'POST'],
},
context,
});
8 changes: 6 additions & 2 deletions tests/archive-node-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test, describe } from 'vitest';
import { expect, test, describe, beforeAll } from 'vitest';
import postgres from 'postgres';

import database_mock from './mocked_sql/database_mock.json';
@@ -47,9 +47,13 @@ class ArchiveNodeAdapterExtend extends ArchiveNodeAdapter {
}
}

const archiveNodeAdapter = new ArchiveNodeAdapterExtend(PG_CONN);
let archiveNodeAdapter;

describe('ArchiveNodeAdapter', async () => {
beforeAll(() => {
archiveNodeAdapter = new ArchiveNodeAdapterExtend(PG_CONN);
});

describe('partitionBlocks', async () => {
test('partitionBlocks should return a non-empty map', async () => {
const blocksMap = archiveNodeAdapter.partitionBlocksExtended(