-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: better database adapter error handling (#101)
- Loading branch information
Showing
15 changed files
with
563 additions
and
80 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
170 changes: 170 additions & 0 deletions
170
packages/entity-database-adapter-knex/src/__integration-tests__/errors-test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
50 changes: 50 additions & 0 deletions
50
packages/entity-database-adapter-knex/src/errors/wrapNativePostgresCall.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.