Skip to content

Commit

Permalink
feat: better database adapter error handling (#101)
Browse files Browse the repository at this point in the history
  • Loading branch information
wschurman authored Dec 23, 2020
1 parent b8e07f9 commit 5208aee
Show file tree
Hide file tree
Showing 15 changed files with 563 additions and 80 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"rules": {
"no-restricted-imports": ["error",{ "paths": [".", "..", "../.."] }],
"tsdoc/syntax": "warn",
"no-console": "warn"
"no-console": "warn",
"handle-callback-err": "off"
},
"overrides": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
} from '@expo/entity';
import Knex from 'knex';

import wrapNativePostgresCall from './errors/wrapNativePostgresCall';

export default class PostgresEntityDatabaseAdapter<TFields> extends EntityDatabaseAdapter<TFields> {
protected getFieldTransformerMap(): FieldTransformerMap {
return new Map<string, FieldTransformer<any>>([
Expand Down Expand Up @@ -43,10 +45,12 @@ export default class PostgresEntityDatabaseAdapter<TFields> extends EntityDataba
tableField: string,
tableValues: readonly any[]
): Promise<object[]> {
return await queryInterface
.select()
.from(tableName)
.whereRaw('?? = ANY(?)', [tableField, tableValues as any[]]);
return await wrapNativePostgresCall(
queryInterface
.select()
.from(tableName)
.whereRaw('?? = ANY(?)', [tableField, tableValues as any[]])
);
}

private applyQueryModifiersToQuery(
Expand Down Expand Up @@ -98,7 +102,7 @@ export default class PostgresEntityDatabaseAdapter<TFields> extends EntityDataba
}

query = this.applyQueryModifiersToQuery(query, querySelectionModifiers);
return await query;
return await wrapNativePostgresCall(query);
}

protected async fetchManyByRawWhereClauseInternalAsync(
Expand All @@ -113,15 +117,17 @@ export default class PostgresEntityDatabaseAdapter<TFields> extends EntityDataba
.from(tableName)
.whereRaw(rawWhereClause, bindings as any);
query = this.applyQueryModifiersToQuery(query, querySelectionModifiers);
return await query;
return await wrapNativePostgresCall(query);
}

protected async insertInternalAsync(
queryInterface: Knex,
tableName: string,
object: object
): Promise<object[]> {
return await queryInterface.insert(object).into(tableName).returning('*');
return await wrapNativePostgresCall(
queryInterface.insert(object).into(tableName).returning('*')
);
}

protected async updateInternalAsync(
Expand All @@ -131,11 +137,9 @@ export default class PostgresEntityDatabaseAdapter<TFields> extends EntityDataba
id: any,
object: object
): Promise<object[]> {
return await queryInterface
.update(object)
.into(tableName)
.where(tableIdField, id)
.returning('*');
return await wrapNativePostgresCall(
queryInterface.update(object).into(tableName).where(tableIdField, id).returning('*')
);
}

protected async deleteInternalAsync(
Expand All @@ -144,6 +148,8 @@ export default class PostgresEntityDatabaseAdapter<TFields> extends EntityDataba
tableIdField: string,
id: any
): Promise<number> {
return await queryInterface.into(tableName).where(tableIdField, id).del();
return await wrapNativePostgresCall(
queryInterface.into(tableName).where(tableIdField, id).del()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { ViewerContext } from '@expo/entity';
import {
EntityDatabaseAdapterCheckConstraintError,
EntityDatabaseAdapterExclusionConstraintError,
EntityDatabaseAdapterForeignKeyConstraintError,
EntityDatabaseAdapterNotNullConstraintError,
EntityDatabaseAdapterTransientError,
EntityDatabaseAdapterUniqueConstraintError,
EntityDatabaseAdapterUnknownError,
} from '@expo/entity/build/errors/EntityDatabaseAdapterError';
import Knex from 'knex';

import ErrorsTestEntity from '../testfixtures/ErrorsTestEntity';
import { createKnexIntegrationTestEntityCompanionProvider } from '../testfixtures/createKnexIntegrationTestEntityCompanionProvider';

describe('postgres errors', () => {
let knexInstance: Knex;

beforeAll(() => {
knexInstance = Knex({
client: 'pg',
connection: {
user: process.env.PGUSER,
password: process.env.PGPASSWORD,
host: 'localhost',
port: parseInt(process.env.PGPORT!, 10),
database: process.env.PGDATABASE,
},
});
});

beforeEach(async () => {
await ErrorsTestEntity.createOrTruncatePostgresTable(knexInstance);
});

afterAll(async () => {
await ErrorsTestEntity.dropPostgresTable(knexInstance);
await knexInstance.destroy();
});

it('throws EntityDatabaseAdapterTransientError on Knex timeout', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.enforceCreateAsync();

const shortTimeoutKnexInstance = Knex({
client: 'pg',
connection: {
user: process.env.PGUSER,
password: process.env.PGPASSWORD,
host: 'localhost',
port: parseInt(process.env.PGPORT!, 10),
database: process.env.PGDATABASE,
},
acquireConnectionTimeout: 1,
});
const vc2 = new ViewerContext(
createKnexIntegrationTestEntityCompanionProvider(shortTimeoutKnexInstance)
);
await expect(ErrorsTestEntity.loader(vc2).enforcing().loadByIDAsync(1)).rejects.toThrow(
EntityDatabaseAdapterTransientError
);
await shortTimeoutKnexInstance.destroy();
});

it('throws EntityDatabaseAdapterNotNullConstraintError when not null is violated', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await expect(
ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', null as any)
.enforceCreateAsync()
).rejects.toThrow(EntityDatabaseAdapterNotNullConstraintError);
});

it('throws EntityDatabaseAdapterForeignKeyConstraintError when foreign key is violated', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await expect(
ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.setField('fieldForeignKey', 2)
.enforceCreateAsync()
).rejects.toThrow(EntityDatabaseAdapterForeignKeyConstraintError);
});

it('throws EntityDatabaseAdapterUniqueConstraintError when primary key unique constraint is violated', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));

await ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.enforceCreateAsync();

await expect(
ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.enforceCreateAsync()
).rejects.toThrow(EntityDatabaseAdapterUniqueConstraintError);
});

it('throws EntityDatabaseAdapterUniqueConstraintError when unique constraint is violated', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await ErrorsTestEntity.creator(vc)
.setField('id', 2)
.setField('fieldNonNull', 'hello')
.setField('fieldUnique', 'hello')
.enforceCreateAsync();

await expect(
ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.setField('fieldUnique', 'hello')
.enforceCreateAsync()
).rejects.toThrow(EntityDatabaseAdapterUniqueConstraintError);
});

it('throws EntityDatabaseAdapterCheckConstraintError when check constraint is violated', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await expect(
ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.setField('checkLessThan5', 2)
.enforceCreateAsync()
).resolves.toBeTruthy();

await expect(
ErrorsTestEntity.creator(vc)
.setField('id', 2)
.setField('fieldNonNull', 'hello')
.setField('checkLessThan5', 10)
.enforceCreateAsync()
).rejects.toThrow(EntityDatabaseAdapterCheckConstraintError);
});

it('throws EntityDatabaseAdapterExclusionConstraintError when exclusion constraint is violated', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await expect(
ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.setField('fieldExclusion', 'what')
.enforceCreateAsync()
).resolves.toBeTruthy();

await expect(
ErrorsTestEntity.creator(vc)
.setField('id', 2)
.setField('fieldNonNull', 'hello')
.setField('fieldExclusion', 'what')
.enforceCreateAsync()
).rejects.toThrow(EntityDatabaseAdapterExclusionConstraintError);
});

it('throws EntityDatabaseAdapterUnknownError otherwise', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await expect(
ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.setField('nonExistentColumn', 'what')
.enforceCreateAsync()
).rejects.toThrow(EntityDatabaseAdapterUnknownError);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { EntityDatabaseAdapterError } from '@expo/entity';
import {
EntityDatabaseAdapterCheckConstraintError,
EntityDatabaseAdapterExclusionConstraintError,
EntityDatabaseAdapterForeignKeyConstraintError,
EntityDatabaseAdapterNotNullConstraintError,
EntityDatabaseAdapterTransientError,
EntityDatabaseAdapterUniqueConstraintError,
EntityDatabaseAdapterUnknownError,
} from '@expo/entity/build/errors/EntityDatabaseAdapterError';
import { KnexTimeoutError } from 'knex';

function wrapNativePostgresError(
error: Error & { code: string | undefined }
): EntityDatabaseAdapterError & Error {
const ret = translatePostgresError(error);
ret.stack = error.stack;
return ret;
}

function translatePostgresError(
error: Error & { code: string | undefined }
): EntityDatabaseAdapterError & Error {
if (error instanceof KnexTimeoutError) {
return new EntityDatabaseAdapterTransientError(error.message, error);
}

switch (error.code) {
case '23502':
return new EntityDatabaseAdapterNotNullConstraintError(error.message, error);
case '23503':
return new EntityDatabaseAdapterForeignKeyConstraintError(error.message, error);
case '23505':
return new EntityDatabaseAdapterUniqueConstraintError(error.message, error);
case '23514':
return new EntityDatabaseAdapterCheckConstraintError(error.message, error);
case '23P01':
return new EntityDatabaseAdapterExclusionConstraintError(error.message, error);
default:
return new EntityDatabaseAdapterUnknownError(error.message, error);
}
}

export default async function wrapNativePostgresCall<T>(fn: Promise<T>): Promise<T> {
try {
return await fn;
} catch (e) {
throw wrapNativePostgresError(e);
}
}
Loading

0 comments on commit 5208aee

Please sign in to comment.