forked from prisma/prisma-engines
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(js-connectors): Proper JS error propagation (prisma#4186)
* feat(js-connectors): Proper JS error propogation The way it is implemented: 1. On TS side, we catch all errors from connector methods. In case we encounterd an error, we store it on per-connector error registry and return it's numeric id to the engine. 2. Engine propogates that numeric id all the way throuhg quaint up to query-engine-node-api, where it becomes new kind of error, `ExternalError` with a code of P2036 and error id in the meta field. 3. Client is then expected to pick the error from the registry and re-throw it. This PR implements this for smoke-tests, similar implementation needs to be done in the client. Implementation is done this way, rather than propogating `napi::Error` instance, to avoid dependencies on napi types in all intermediate crates other than `js-connectors` and `query-engine-node-api`. To better accomodate this pattern, `Result` type was introduced to the connectors. It allows to explicitly indicate if particular call succeeded or failed via return value. On Rust side, those values get parsed and converted into `std::result::Result` values. On Rust side, `AsyncJsFunction` wrapper over `ThreadsafeFunction` is introduced. It handles promises and JS result type conversions automatically and simplifies `Proxy` code. This also lays the foundation for supporting "Known" errors - JsError type could be extended in a future with new error types, that could be converted into standard prisma errors with normal error codes. Fix #prisma/team-orm#260 * Address review feedback --------- Co-authored-by: jkomyno <[email protected]>
- Loading branch information
Showing
18 changed files
with
410 additions
and
192 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
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
67 changes: 54 additions & 13 deletions
67
query-engine/js-connectors/js/js-connector-utils/src/binder.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 |
---|---|---|
@@ -1,23 +1,64 @@ | ||
import type { Connector, Transaction } from './types'; | ||
import type { ErrorCapturingConnector, Connector, Transaction, ErrorRegistry, ErrorRecord, Result } from './types'; | ||
|
||
|
||
class ErrorRegistryInternal implements ErrorRegistry { | ||
private registeredErrors: ErrorRecord[] = [] | ||
|
||
consumeError(id: number): ErrorRecord | undefined { | ||
return this.registeredErrors[id] | ||
} | ||
|
||
registerNewError(error: unknown) { | ||
let i=0; | ||
while (this.registeredErrors[i] !== undefined) { | ||
i++ | ||
} | ||
this.registeredErrors[i] = { error } | ||
return i | ||
} | ||
|
||
} | ||
|
||
// *.bind(connector) is required to preserve the `this` context of functions whose | ||
// execution is delegated to napi.rs. | ||
export const bindConnector = (connector: Connector): Connector => ({ | ||
queryRaw: connector.queryRaw.bind(connector), | ||
executeRaw: connector.executeRaw.bind(connector), | ||
flavour: connector.flavour, | ||
startTransaction: connector.startTransaction.bind(connector), | ||
close: connector.close.bind(connector) | ||
}) | ||
export const bindConnector = (connector: Connector): ErrorCapturingConnector => { | ||
const errorRegistry = new ErrorRegistryInternal() | ||
|
||
return { | ||
errorRegistry, | ||
queryRaw: wrapAsync(errorRegistry, connector.queryRaw.bind(connector)), | ||
executeRaw: wrapAsync(errorRegistry, connector.executeRaw.bind(connector)), | ||
flavour: connector.flavour, | ||
startTransaction: async (...args) => { | ||
const result = await connector.startTransaction(...args); | ||
if (result.ok) { | ||
return { ok: true, value: bindTransaction(errorRegistry, result.value)} | ||
} | ||
return result | ||
}, | ||
close: wrapAsync(errorRegistry, connector.close.bind(connector)) | ||
} | ||
} | ||
|
||
// *.bind(transaction) is required to preserve the `this` context of functions whose | ||
// execution is delegated to napi.rs. | ||
export const bindTransaction = (transaction: Transaction): Transaction => { | ||
const bindTransaction = (errorRegistry: ErrorRegistryInternal, transaction: Transaction): Transaction => { | ||
return ({ | ||
flavour: transaction.flavour, | ||
queryRaw: transaction.queryRaw.bind(transaction), | ||
executeRaw: transaction.executeRaw.bind(transaction), | ||
commit: transaction.commit.bind(transaction), | ||
rollback: transaction.rollback.bind(transaction) | ||
queryRaw: wrapAsync(errorRegistry, transaction.queryRaw.bind(transaction)), | ||
executeRaw: wrapAsync(errorRegistry, transaction.executeRaw.bind(transaction)), | ||
commit: wrapAsync(errorRegistry, transaction.commit.bind(transaction)), | ||
rollback: wrapAsync(errorRegistry, transaction.rollback.bind(transaction)) | ||
}); | ||
} | ||
|
||
function wrapAsync<A extends unknown[], R>(registry: ErrorRegistryInternal, fn: (...args: A) => Promise<Result<R>>): (...args: A) => Promise<Result<R>> { | ||
return async (...args) => { | ||
try { | ||
return await fn(...args) | ||
} catch (error) { | ||
const id = registry.registerNewError(error) | ||
return { ok: false, error: { kind: 'GenericJsError', id } } | ||
} | ||
} | ||
} |
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 |
---|---|---|
@@ -1,4 +1,4 @@ | ||
export { bindConnector, bindTransaction } from './binder' | ||
export { bindConnector } from './binder' | ||
export { ColumnTypeEnum } from './const' | ||
export { Debug } from './debug' | ||
export type * from './types' |
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
Oops, something went wrong.