@embracesql/postgres
Advanced tools
Comparing version 0.1.2 to 0.1.3
{ | ||
"name": "@embracesql/postgres", | ||
"version": "0.1.2", | ||
"version": "0.1.3", | ||
"description": "EmbraceSQL shared library for talking to postgres. Used in Node.", | ||
@@ -13,3 +13,3 @@ "type": "module", | ||
"dependencies": { | ||
"@embracesql/shared": "^0.1.2", | ||
"@embracesql/shared": "^0.1.3", | ||
"glob": "^10.3.10", | ||
@@ -23,3 +23,3 @@ "object-hash": "^3.0.0", | ||
}, | ||
"gitHead": "8f9395afff4f331cde0761a283fea630c2d43a4f" | ||
"gitHead": "edeebbd5811d60be38bba6c04df81b7013349f1a" | ||
} |
@@ -207,3 +207,3 @@ import { PGIndexes } from "./generator/pgtype/pgindex"; | ||
i, | ||
context.database.resolveType(a.type)!, | ||
context.database.resolveType(a.type), | ||
true, | ||
@@ -232,3 +232,3 @@ true, | ||
i, | ||
context.database.resolveType(a)!, | ||
context.database.resolveType(a), | ||
true, | ||
@@ -310,2 +310,8 @@ false, | ||
transform: { undefined: null }, | ||
// let's be nearly stateless on connections and not leave | ||
// them in the pool for long by default -- this better deals | ||
// with transient network errors -- avoids 'torn' connections in the pool | ||
idle_timeout: 30, | ||
max_lifetime: 30, | ||
...(props ?? {}), | ||
}); | ||
@@ -312,0 +318,0 @@ |
@@ -117,3 +117,3 @@ import { Context, PostgresProcTypecast } from "../../../context"; | ||
.forEach((oid, i) => { | ||
const type = context.database.resolveType(oid)!; | ||
const type = context.database.resolveType(oid); | ||
new AttributeNode( | ||
@@ -241,3 +241,3 @@ parametersType, | ||
.map((oid, i) => { | ||
const type = context.database.resolveType(oid)!; | ||
const type = context.database.resolveType(oid); | ||
return { | ||
@@ -244,0 +244,0 @@ name: this.proc.proc.proargnames[i], |
@@ -24,3 +24,3 @@ import { PGCatalogType } from "./pgcatalogtype"; | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
const type = context.database.resolveType(this.rngsubtype)!; | ||
const type = context.database.resolveType(this.rngsubtype); | ||
// array with two elements is the nearst-to-a-tuple in JS | ||
@@ -27,0 +27,0 @@ return `[${type.typescriptNamespacedName}, ${type.typescriptNamespacedName}]`; |
128
src/index.ts
import { Context } from "./context"; | ||
import databaseRole from "./middleware/role"; | ||
import { MiddlewareContext, MiddlewareDispatcher } from "./middleware/types"; | ||
import { EmbraceSQLInvocation, EmbraceSQLOptions } from "@embracesql/shared"; | ||
import { | ||
EmbraceSQLInvocation, | ||
EmbraceSQLOptions, | ||
InvokeQueryOptions, | ||
sleep, | ||
} from "@embracesql/shared"; | ||
import postgres from "postgres"; | ||
@@ -23,3 +28,5 @@ | ||
/** | ||
* A single postgres database. Inherit from this in generated code. | ||
* A single postgres database. | ||
* | ||
* Code generation extends this class, making it specific to your database. | ||
*/ | ||
@@ -36,9 +43,9 @@ export abstract class PostgresDatabase< | ||
/** | ||
* Clean up the connection. | ||
* Call when you are all finished talking to the database. | ||
*/ | ||
public async disconnect() { | ||
await this.context.sql.end(); | ||
await this.context.sql.end({ timeout: 1 }); | ||
} | ||
get cls(): ConstructorOf<this> { | ||
private get cls(): ConstructorOf<this> { | ||
const current = Object.getPrototypeOf(this).constructor; | ||
@@ -48,6 +55,2 @@ return current; | ||
get self(): this { | ||
return this; | ||
} | ||
get typed(): TTypecast { | ||
@@ -62,3 +65,4 @@ return this.context.sql.typed as unknown as TTypecast; | ||
async beginTransaction() { | ||
const myself = this.self; | ||
// eslint-disable-next-line @typescript-eslint/no-this-alias | ||
const myself = this; | ||
const CurrentSubclass = this.cls; | ||
@@ -71,13 +75,19 @@ return await new Promise<{ | ||
const complete = new Promise((resolve, reject) => { | ||
this.context.sql | ||
.begin(async (sql) => { | ||
resolveReady({ | ||
// this is 'the change' -- adding in a new sql that is in transaction | ||
database: new CurrentSubclass({ ...this.context, sql }), | ||
commit: () => resolve(true), | ||
rollback: (message?: string) => reject(message), | ||
}); | ||
await complete; | ||
}) | ||
.catch((reason) => reason); | ||
// postgres doesn't really have 'nested' transactions -- it has savepoints | ||
// so check if we are 'in' a transaction | ||
const begin = | ||
this.context.sql.begin?.bind(this.context.sql) ?? | ||
(this.context.sql as postgres.TransactionSql).savepoint.bind( | ||
this.context.sql, | ||
); | ||
begin(async (sql) => { | ||
resolveReady({ | ||
// this is 'the change' -- adding in a new sql that is in transaction | ||
database: new CurrentSubclass({ ...this.context, sql }), | ||
commit: () => resolve(true), | ||
rollback: (message?: string) => reject(message), | ||
}); | ||
await complete; | ||
}).catch((reason) => reason); | ||
}); | ||
@@ -94,16 +104,20 @@ }); | ||
async withTransaction<T>(body: (database: this) => Promise<T>) { | ||
const CurrentSubclass = this.cls; | ||
if (this.context.sql.begin !== undefined) { | ||
// root transaction | ||
return await this.context.sql.begin( | ||
async (sql) => | ||
await body(new CurrentSubclass({ ...this.context, sql })), | ||
); | ||
} else { | ||
// nested transaction | ||
const nested = this.context.sql as postgres.TransactionSql; | ||
return await nested.savepoint( | ||
async (sql) => | ||
await body(new CurrentSubclass({ ...this.context, sql })), | ||
); | ||
const { database, commit, rollback } = await this.beginTransaction(); | ||
try { | ||
const ret = await body(database); | ||
commit(); | ||
return ret; | ||
} catch (e) { | ||
// the postgres driver has a flaw - if you encounter a fatal connection | ||
// error inside a transaction, such that `rollback` cannot be run | ||
// because the connection is dead -- the driver hangs awaiting a `rollback` | ||
// that simply cannot happen | ||
if ((e as postgres.PostgresError)?.severity === "FATAL") { | ||
// the error is the return value -- yeah a little odd | ||
// but this gets us past the rollback that won't roll back | ||
throw e; | ||
} else { | ||
rollback(); | ||
throw e; | ||
} | ||
} | ||
@@ -126,3 +140,3 @@ } | ||
V = never, | ||
O extends EmbraceSQLOptions = EmbraceSQLOptions, | ||
O extends InvokeQueryOptions = InvokeQueryOptions, | ||
>( | ||
@@ -133,4 +147,6 @@ queryCallback: InvokeQuery<R, P, V, O>, | ||
): Promise<R> { | ||
// if retries are not specific -- now is the time | ||
// here is the middleware run stack | ||
const runStack = async (database: this) => { | ||
const runStack = async (database: this, retry: number) => { | ||
// pick up the headers | ||
@@ -146,2 +162,3 @@ // first the middleware | ||
>, | ||
retry, | ||
}); | ||
@@ -151,15 +168,30 @@ return queryCallback(database.context.sql, request); | ||
// need a reserved or single connection throughout | ||
// so that all effects are applied to the same session | ||
if (this.context.sql.begin !== undefined) { | ||
return this.withTransaction(async (database) => { | ||
return runStack(database); | ||
}) as R; | ||
} else { | ||
// if we are in a transaction, there won't be an option to reserve | ||
// but the good news is the driver has already reserved a connection | ||
// for the scope of the transaction | ||
return runStack(this); | ||
let finalError: Error | undefined = undefined; | ||
for (let retry = 0; retry <= (request?.options?.retries ?? 0); retry += 1) { | ||
try { | ||
// need a reserved or single connection throughout | ||
// so that all effects are applied to the same session | ||
if (this.context.sql.begin !== undefined) { | ||
// make sure to await here -- need to catch what might be thrown | ||
return (await this.withTransaction(async (database) => { | ||
return await runStack(database, retry); | ||
})) as R; | ||
} else { | ||
// if we are in a transaction, there won't be an option to reserve | ||
// but the good news is the driver has already reserved a connection | ||
// for the scope of the transaction | ||
return await runStack(this, retry); | ||
} | ||
} catch (e) { | ||
finalError = e as Error; | ||
// this is the exponential backoff part | ||
if (retry > 0) { | ||
await sleep(100 * 2 ** retry); | ||
} | ||
} | ||
} | ||
// if we got here, we're past all the retries and it is time to throw | ||
throw finalError; | ||
} | ||
} |
@@ -8,3 +8,11 @@ import { Context } from "../context"; | ||
export type MiddlewareContext = Context & { | ||
/** | ||
* Request for a query. Modify this with the middleware chain and | ||
* the final modified version will be used to invoke your actual query. | ||
*/ | ||
request?: EmbraceSQLInvocation<object, object, InvokeQueryOptions>; | ||
/** | ||
* Current retry with 0 indicating we haven't retried ... yet. | ||
*/ | ||
retry: number; | ||
}; | ||
@@ -11,0 +19,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
151772
4509
Updated@embracesql/shared@^0.1.3