@aventara/prisma7-adapter 1.0.0-rc.12 → 1.0.0-rc.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # @aventara/prisma7-adapter
2
2
 
3
+ ## 1.0.0-rc.14
4
+
5
+ ### Patch Changes
6
+
7
+ - 28688e4: On SQLite (`@prisma/adapter-better-sqlite3`), a plain operation or `ctx.$sql` that gives up waiting for the connection another request's transaction holds still answers `A3002`, and its diagnostic now says so: how many milliseconds it waited, the `maxWait` that ran out (the adapter's `transactionOptions.maxWait`, or else the PrismaClient's, 2000 ms unless set on the client), and that it never ran. A transaction that fails to open for another reason, such as an unsupported isolation level, keeps its usual diagnostic. Before, the diagnostic said the call's transaction "was no longer open", which named the wrong cause. Raise the adapter's `transactionOptions.maxWait`, or keep the transaction that holds the connection shorter.
8
+ - Updated dependencies [1c5eaa3]
9
+ - @aventara/core@1.0.0-rc.14
10
+
11
+ ## 1.0.0-rc.13
12
+
13
+ ### Patch Changes
14
+
15
+ - 66b31b5: With SQLite through `@prisma/adapter-better-sqlite3`, a plain operation no longer runs inside another request's open
16
+ transaction. That driver has one connection, and an interactive transaction holds it while it waits between
17
+ statements; Prisma ran a plain statement on that same connection without waiting, so an unforced write made meanwhile
18
+ rolled back with the other request's transaction, and an unforced read saw its uncommitted rows. With that driver, an
19
+ operation that is not part of a transaction, and a `ctx.$sql` that is not, now each run in a transaction of their own
20
+ that holds only that call, so they wait until the open transaction ends. `transactionOptions` apply to that wait: past
21
+ `maxWait` the operation answers `A3002`, and `timeout` limits a single plain operation too. A plain call made from
22
+ inside a transaction's own async context (a stage calling the root Framework instead of its `ctx`) joins that
23
+ transaction instead of waiting on it, and warns (`AVENTARA_PRISMA_SQLITE_TRANSACTION_JOINED`) once per Resource and operation and once for `ctx.$sql`,
24
+ because a helper shared across requests can carry that context into another request's call. PostgreSQL and D1 run as
25
+ before. A mismatched `driver` declaration is now refused at construction: `createPrismaAdapter` throws when the Prisma
26
+ client was constructed with another driver adapter than the declared `driver`, and names the supported set
27
+ (`@prisma/adapter-better-sqlite3`, `@prisma/adapter-pg`, `@prisma/adapter-d1`); a client built with any other driver
28
+ adapter is not supported.
29
+ - Updated dependencies [6c3e25c]
30
+ - Updated dependencies [2aaa27d]
31
+ - Updated dependencies [f3b291f]
32
+ - Updated dependencies [2aaa27d]
33
+ - Updated dependencies [e4e8abd]
34
+ - Updated dependencies [e662048]
35
+ - Updated dependencies [f3b291f]
36
+ - Updated dependencies [f3b291f]
37
+ - Updated dependencies [6c3e25c]
38
+ - Updated dependencies [6c3e25c]
39
+ - Updated dependencies [a708188]
40
+ - Updated dependencies [a9260d2]
41
+ - @aventara/core@1.0.0-rc.13
42
+
3
43
  ## 1.0.0-rc.12
4
44
 
5
45
  ### Patch Changes
package/README.md CHANGED
@@ -50,7 +50,10 @@ const framework = await createFramework({ api: "/api", adapter });
50
50
 
51
51
  The caller declares `provider` and `driver`. The adapter does not inspect client internals to guess
52
52
  them: the driver decides whether transactions are advertised at all, and a wrong guess would be worse
53
- than a declaration that is checked.
53
+ than a declaration that is checked. It does check the declaration: when the client names the driver
54
+ adapter it was constructed with (Prisma 7.10 keeps it where its own D1 check reads it), a `driver` that
55
+ differs refuses to start (`AvPrismaDiscoveryArtifactError`, naming both). A client that does not say,
56
+ such as a test double, is not compared.
54
57
 
55
58
  `createPrismaAdapter` is `async`. Construction verifies the discovery artifact, and the digest it
56
59
  checks is a `crypto.subtle` promise, so every check runs once, here, before an adapter exists.
@@ -90,6 +93,62 @@ The supported isolation levels come from the declared provider. PostgreSQL admit
90
93
  `ReadCommitted`, `RepeatableRead` and `Serializable`; SQLite admits `Serializable` only. Asking for a
91
94
  level the provider does not support fails the transaction; see [`P2028`](#p2028-covers-seven-situations-and-three-of-them-are-separable).
92
95
 
96
+ ### SQLite: one connection
97
+
98
+ `@prisma/adapter-better-sqlite3` holds one connection, and an interactive transaction holds it from `BEGIN`
99
+ to `COMMIT`, including while it waits between statements. Prisma runs a plain statement on that same
100
+ connection without waiting, so a plain call made meanwhile would run inside the open transaction: it would
101
+ see its uncommitted rows and roll back with it. With that driver, an operation that is not part of a
102
+ transaction, and a `ctx.$sql` that is not, each run in a `$transaction` of their own that holds only that
103
+ call. They wait on the driver's own lock, so operations and transactions queue for the connection and run
104
+ one at a time. `transactionOptions` apply to the wait: past `maxWait` the operation answers `A3002`, its
105
+ diagnostic says how long it waited for the connection another request's transaction held, and
106
+ `timeout` limits a single plain operation too. `@prisma/adapter-pg` checks a connection of its own out of
107
+ the pool for each transaction, and D1 runs none, so both stay as they were. Calls made on the Prisma client
108
+ directly, outside Aventara, are not covered.
109
+
110
+ #### A call from inside a transaction joins it, and warns
111
+
112
+ A plain call made from inside an open transaction's own async context, such as a stage that calls the root
113
+ Framework instead of its `ctx`, would otherwise queue behind the very transaction it runs in and fail after
114
+ `maxWait`. So it joins that transaction instead: it runs on the transaction's connection and commits or
115
+ rolls back with it, as it did before the queue existed. The adapter knows which transaction a call belongs
116
+ to only from Node's `AsyncLocalStorage`. The first time a given Resource and operation joins, the process
117
+ warns once (and once for `ctx.$sql`):
118
+
119
+ ```text
120
+ (node:4711) [AVENTARA_PRISMA_SQLITE_TRANSACTION_JOINED] AventaraWarning: Author create.many ran inside an
121
+ open SQLite transaction (better-sqlite3) instead of waiting for it: it was called from that transaction's
122
+ async context, so it joined the transaction and commits or rolls back with it. …
123
+ ```
124
+
125
+ A join you meant (the stage above) is fine, and the warning is expected for it. A join you did not mean
126
+ comes from a helper shared across requests that runs another request's work in the async context it was
127
+ first called from. A naive batcher is the usual one:
128
+
129
+ ```ts
130
+ let pending: Array<{ email: string }> = [];
131
+ let flushing: ReturnType<typeof setTimeout> | undefined;
132
+
133
+ export function addAuthor(email: string) {
134
+ pending.push({ email });
135
+ // Scheduled inside request A's transaction (say, from an after hook), the timer
136
+ // keeps A's async context. Request B's author, added before the flush, is then
137
+ // written inside A's transaction and rolls back if A does.
138
+ flushing ??= setTimeout(() => {
139
+ const data = pending;
140
+ pending = [];
141
+ flushing = undefined;
142
+ void framework.application.Author.create.many({ data });
143
+ }, 50);
144
+ }
145
+ ```
146
+
147
+ Give such a helper its own context: bind its callback with `AsyncResource.bind` from `node:async_hooks`
148
+ when it is created (`const flush = AsyncResource.bind(() => { … })`, created at module load, outside any
149
+ transaction), or schedule its timers from outside a transaction. The join is never refused, because code
150
+ that joins on purpose has to keep working.
151
+
93
152
  ---
94
153
 
95
154
  ## The generate step, and the artifact it emits
@@ -0,0 +1,2 @@
1
+ /** Emits an `AventaraWarning` with `code`, or logs it where there is no `process.emitWarning`. */
2
+ export declare function emitAdapterWarning(message: string, code: string): void;
@@ -0,0 +1,8 @@
1
+ export function emitAdapterWarning(message, code) {
2
+ const emitWarning = globalThis.process?.emitWarning;
3
+ if (typeof emitWarning === "function") {
4
+ emitWarning(message, { type: "AventaraWarning", code });
5
+ return;
6
+ }
7
+ globalThis.console?.warn(`AventaraWarning: ${message}`);
8
+ }
@@ -6,3 +6,11 @@ import type { PrismaClientShape } from "./discovery-artifact.interface.js";
6
6
  * @throws AvPrismaDiscoveryArtifactError
7
7
  */
8
8
  export declare function assertClientMatchesArtifact(recorded: PrismaClientShape, client: unknown): void;
9
+ /**
10
+ * Throws when the Prisma client in hand was constructed with another driver
11
+ * adapter than the declared `driver` (one outside the supported set included);
12
+ * skips when the client does not say.
13
+ *
14
+ * @throws AvPrismaDiscoveryArtifactError
15
+ */
16
+ export declare function assertClientUsesDeclaredDriver(driver: string, client: unknown): void;
@@ -1,4 +1,5 @@
1
1
  import { delegateKeyOf } from "../execution/operation.executor.js";
2
+ import { PRISMA_DRIVERS } from "../provider/provider-profile.js";
2
3
  import { AvPrismaDiscoveryArtifactError } from "./artifact.loader.js";
3
4
  const SHOWN = 3;
4
5
  function readMember(value, key) {
@@ -91,3 +92,16 @@ export function assertClientMatchesArtifact(recorded, client) {
91
92
  const more = drift.length > SHOWN ? `; and ${drift.length - SHOWN} more` : "";
92
93
  throw new AvPrismaDiscoveryArtifactError(`The Prisma client has changed since the discovery artifact was generated (${shown}${more}):`, "run `npm run aventara:prepare` (the adapter's `aventara-prisma7-generate`) to regenerate it, then restart.");
93
94
  }
95
+ const SUPPORTED_DRIVERS = `${PRISMA_DRIVERS.slice(0, -1)
96
+ .map((name) => `\`${name}\``)
97
+ .join(", ")} and \`${PRISMA_DRIVERS[PRISMA_DRIVERS.length - 1]}\``;
98
+ export function assertClientUsesDeclaredDriver(driver, client) {
99
+ const constructedWith = readMember(readMember(readMember(client, "_engineConfig"), "adapter"), "adapterName");
100
+ if (typeof constructedWith !== "string" || constructedWith === driver) {
101
+ return;
102
+ }
103
+ const supported = PRISMA_DRIVERS.includes(constructedWith);
104
+ throw new AvPrismaDiscoveryArtifactError(`The Prisma client is constructed with the driver adapter \`${constructedWith}\`, but createPrismaAdapter was given \`driver: "${driver}"\`.`, `@aventara/prisma7-adapter supports the driver adapters ${SUPPORTED_DRIVERS}; ${supported
105
+ ? `declare \`driver: "${constructedWith}"\`, then restart.`
106
+ : `a client constructed with any other driver adapter is not supported.`}`);
107
+ }
@@ -66,6 +66,21 @@ export declare function classifyPrismaError(error: unknown, operation: Pick<AvOp
66
66
  export declare function classifyPrismaStatementError(error: unknown): PrismaErrorClassification;
67
67
  /** The deliberate failure an `outcome` classification stands for. */
68
68
  export declare function prismaOutcomeFailure(code: AvOperationErrorCode): AvError;
69
+ /**
70
+ * Whether a failure to open a transaction has the structure of the `maxWait`
71
+ * acquisition timeout: `P2028` with an empty `meta`.
72
+ */
73
+ export declare function isPrismaAcquisitionTimeout(error: unknown): boolean;
74
+ /**
75
+ * `A3002` for a plain call on a one-connection driver that gave up waiting for
76
+ * the connection another request's transaction held: its own transaction never
77
+ * opened, so `OUTCOME_MESSAGES`' "no longer open" text would name the wrong
78
+ * cause. Diagnostics only, like every `A3xxx` text here.
79
+ */
80
+ export declare function prismaConnectionWaitFailure(facts: {
81
+ readonly waitedMs: number;
82
+ readonly maxWait: number | undefined;
83
+ }): AvError;
69
84
  /** The `A2004` a value the database refused answers: one issue at the arguments (or the value in them) saying what was refused. */
70
85
  export declare function prismaValueFailure(refusal: PrismaValueRefusal, path?: PrismaArgumentPath): AvError;
71
86
  /** The `A2004` a nested write that writes its relation back answers: `V1012` at `path`. */
@@ -127,6 +127,18 @@ export function prismaOutcomeFailure(code) {
127
127
  message: OUTCOME_MESSAGES[code] ?? "The operation could not be completed.",
128
128
  });
129
129
  }
130
+ export function isPrismaAcquisitionTimeout(error) {
131
+ const facts = readPrismaError(error);
132
+ return facts.code === "P2028" && facts.metaKeys.length === 0;
133
+ }
134
+ export function prismaConnectionWaitFailure(facts) {
135
+ const limit = facts.maxWait === undefined
136
+ ? "the PrismaClient's maxWait (2000 ms unless set on the client)"
137
+ : `the adapter's maxWait (${facts.maxWait} ms)`;
138
+ return new AvError("A3002", {
139
+ message: `This call waited ${Math.round(facts.waitedMs)} ms for the SQLite connection, which another request's transaction held, and gave up when ${limit} ran out: it never ran and wrote nothing. Raise the adapter's transactionOptions.maxWait, or keep the transaction that holds the connection shorter.`,
140
+ });
141
+ }
130
142
  export function prismaValueFailure(refusal, path = []) {
131
143
  const { code, message } = DATABASE_REFUSALS[refusal];
132
144
  return new AvError("A2004", {
@@ -0,0 +1,25 @@
1
+ import type { AvAdapterOperation } from "@aventara/core/internal";
2
+ import type { AvPrismaDelegateHost, AvPrismaTransactionOptions } from "../prisma-client.interface.js";
3
+ /** What a plain call is, for the warning a join emits: an operation, or a `ctx.$sql` statement. */
4
+ export type PrismaPlainCall = AvAdapterOperation | "ctx.$sql";
5
+ /** How plain work and transactions share a driver's one connection. */
6
+ export type PrismaSharedConnectionRouter = {
7
+ /**
8
+ * Runs plain work on the transaction this async flow is inside (and warns,
9
+ * once per `call`), or else in a one-statement transaction of its own; a
10
+ * failure to open or commit that transaction is classified through the
11
+ * closed table.
12
+ */
13
+ plain<T>(call: PrismaPlainCall, work: (client: AvPrismaDelegateHost) => Promise<T>): Promise<T>;
14
+ /**
15
+ * Runs a transaction's work so that plain work issued from inside it joins
16
+ * it. The work's own promise is returned as is: its rejection reaches Prisma
17
+ * by reference (core recognises its abort sentinel by identity).
18
+ */
19
+ within<T>(transactional: AvPrismaDelegateHost, work: () => Promise<T>): Promise<T>;
20
+ };
21
+ /** The router for a shared-connection driver's client. */
22
+ export declare function createSharedConnectionRouter(dependencies: {
23
+ readonly client: AvPrismaDelegateHost;
24
+ readonly transactionOptions?: AvPrismaTransactionOptions;
25
+ }): PrismaSharedConnectionRouter;
@@ -0,0 +1,80 @@
1
+ import { emitAdapterWarning } from "../adapter-warning.emitter.js";
2
+ import { classifyPrismaStatementError, isPrismaAcquisitionTimeout, prismaConnectionWaitFailure, prismaOutcomeFailure, } from "../errors/error.normalizer.js";
3
+ const TRANSACTION_JOINED_WARNING = "AVENTARA_PRISMA_SQLITE_TRANSACTION_JOINED";
4
+ const warnedJoins = new Set();
5
+ function warnJoined(call) {
6
+ const subject = call === "ctx.$sql"
7
+ ? call
8
+ : `${call.resource} ${call.family}.${call.variant}`;
9
+ if (warnedJoins.has(subject)) {
10
+ return;
11
+ }
12
+ warnedJoins.add(subject);
13
+ emitAdapterWarning(`${subject} ran inside an open SQLite transaction (better-sqlite3) instead of waiting for it: it was called from that transaction's async context, so it joined the transaction and commits or rolls back with it. That is intended when the transaction's own work makes the call, such as a stage calling the root Framework instead of its ctx. If the call came through a helper shared across requests (a batcher, a queue, a timer first scheduled inside a transaction), it is another request's call and has joined a transaction that is not its own: bind the helper's callbacks with AsyncResource.bind, or schedule them outside the transaction. ${call === "ctx.$sql" ? "Warned once for ctx.$sql." : "Warned once per Resource and operation."}`, TRANSACTION_JOINED_WARNING);
14
+ }
15
+ function createFlowStorage() {
16
+ const process = globalThis.process;
17
+ const hooks = process?.getBuiltinModule?.("node:async_hooks");
18
+ return typeof hooks?.AsyncLocalStorage === "function"
19
+ ? new hooks.AsyncLocalStorage()
20
+ : undefined;
21
+ }
22
+ export function createSharedConnectionRouter(dependencies) {
23
+ const flows = createFlowStorage();
24
+ const within = (transactional, work) => {
25
+ if (flows === undefined) {
26
+ return work();
27
+ }
28
+ const entry = { client: transactional, open: true };
29
+ const settled = flows.run(entry, work);
30
+ const close = () => {
31
+ entry.open = false;
32
+ };
33
+ settled.then(close, close);
34
+ return settled;
35
+ };
36
+ const plain = async (call, work) => {
37
+ const joined = flows?.getStore();
38
+ if (joined?.open === true) {
39
+ warnJoined(call);
40
+ return work(joined.client);
41
+ }
42
+ let thrownByWork = false;
43
+ let started = false;
44
+ const run = async (transactional) => {
45
+ started = true;
46
+ try {
47
+ return await within(transactional, () => work(transactional));
48
+ }
49
+ catch (error) {
50
+ thrownByWork = true;
51
+ throw error;
52
+ }
53
+ };
54
+ const options = dependencies.transactionOptions;
55
+ const waitedFrom = performance.now();
56
+ try {
57
+ return await (options === undefined
58
+ ? dependencies.client.$transaction(run)
59
+ : dependencies.client.$transaction(run, options));
60
+ }
61
+ catch (error) {
62
+ if (thrownByWork) {
63
+ throw error;
64
+ }
65
+ const classification = classifyPrismaStatementError(error);
66
+ if (classification.kind === "outcome") {
67
+ const waitedMs = performance.now() - waitedFrom;
68
+ const maxWait = options?.maxWait;
69
+ const waitedOut = !started &&
70
+ isPrismaAcquisitionTimeout(error) &&
71
+ (maxWait === undefined || waitedMs + 1 >= maxWait);
72
+ throw waitedOut
73
+ ? prismaConnectionWaitFailure({ waitedMs, maxWait })
74
+ : prismaOutcomeFailure(classification.code);
75
+ }
76
+ throw error;
77
+ }
78
+ };
79
+ return { plain, within };
80
+ }
@@ -4,6 +4,7 @@ import type { PrismaResourceTranslation } from "../discovery/discovery-artifact.
4
4
  import type { PrismaBackReferenceTable } from "../errors/back-reference.locator.js";
5
5
  import type { AvPrismaDelegateHost, AvPrismaTransactionOptions } from "../prisma-client.interface.js";
6
6
  import type { PrismaBigintBinding } from "../provider/provider-profile.js";
7
+ import type { PrismaSharedConnectionRouter } from "./shared-connection.router.js";
7
8
  type PrismaTransactionExecutorDependencies = {
8
9
  readonly client: AvPrismaDelegateHost;
9
10
  readonly model: AvAdapterModel;
@@ -12,6 +13,7 @@ type PrismaTransactionExecutorDependencies = {
12
13
  readonly backReferences: PrismaBackReferenceTable;
13
14
  readonly bigintBinding: PrismaBigintBinding;
14
15
  readonly transactionOptions?: AvPrismaTransactionOptions;
16
+ readonly sharedConnection?: PrismaSharedConnectionRouter;
15
17
  };
16
18
  type PrismaTransactionRunner<M extends AvAdapterModel> = <T>(work: (executor: AvAdapterTransactionExecutor<M>) => Promise<T>) => Promise<T>;
17
19
  export declare function createPrismaTransactionExecutor<M extends AvAdapterModel>(dependencies: PrismaTransactionExecutorDependencies): PrismaTransactionRunner<M>;
@@ -13,7 +13,13 @@ export function createPrismaTransactionExecutor(dependencies) {
13
13
  }),
14
14
  executeSql: createPrismaSqlStatementExecutor(transactional),
15
15
  });
16
- return (work) => options === undefined
17
- ? dependencies.client.$transaction(async (transactional) => work(scopedExecutorFor(transactional)))
18
- : dependencies.client.$transaction(async (transactional) => work(scopedExecutorFor(transactional)), options);
16
+ const router = dependencies.sharedConnection;
17
+ return (work) => {
18
+ const run = async (transactional) => router === undefined
19
+ ? work(scopedExecutorFor(transactional))
20
+ : router.within(transactional, () => work(scopedExecutorFor(transactional)));
21
+ return options === undefined
22
+ ? dependencies.client.$transaction(run)
23
+ : dependencies.client.$transaction(run, options);
24
+ };
19
25
  }
@@ -41,7 +41,8 @@ type PrismaAdapterConstruction<TClient extends AvPrismaDelegateHost, M extends A
41
41
  * ```
42
42
  *
43
43
  * @throws `AvPrismaDiscoveryArtifactError` when the artifact is stale, corrupt or
44
- * was generated for another Prisma major; its message says how to regenerate it.
44
+ * was generated for another Prisma major (its message says how to regenerate
45
+ * it), or when the client was constructed with another driver than `driver`.
45
46
  */
46
47
  export declare function createPrismaAdapter<TClient extends AvPrismaDelegateHost, M extends AvPrismaAdapterModel = AvPrismaAdapterModel>(options: PrismaAdapterConstruction<TClient, M>): Promise<AvAdapter<M>>;
47
48
  export {};
@@ -1,12 +1,14 @@
1
+ import { emitAdapterWarning } from "./adapter-warning.emitter.js";
1
2
  import { loadDiscoveryArtifact } from "./discovery/artifact.loader.js";
2
- import { assertClientMatchesArtifact } from "./discovery/client-shape.verifier.js";
3
+ import { assertClientMatchesArtifact, assertClientUsesDeclaredDriver, } from "./discovery/client-shape.verifier.js";
3
4
  import { locateInstalledPrismaClientVersion } from "./discovery/installed-client.locator.js";
4
5
  import { derivePrismaBackReferences } from "./errors/back-reference.locator.js";
5
6
  import { createPrismaOperationExecutor } from "./execution/operation.executor.js";
7
+ import { createSharedConnectionRouter } from "./execution/shared-connection.router.js";
6
8
  import { createPrismaSqlStatementExecutor } from "./execution/sql-statement.executor.js";
7
9
  import { createPrismaTransactionExecutor } from "./execution/transaction.executor.js";
8
10
  import { describePrismaAdapter } from "./prisma-adapter.description.js";
9
- import { resolveBigintBinding, } from "./provider/provider-profile.js";
11
+ import { resolveBigintBinding, resolveTransactionConnection, } from "./provider/provider-profile.js";
10
12
  const PRISMA_VERSION_UNCHECKED_WARNING = "AVENTARA_PRISMA_VERSION_UNCHECKED";
11
13
  function installedClientVersion(discovery, override) {
12
14
  if (override !== undefined) {
@@ -17,16 +19,7 @@ function installedClientVersion(discovery, override) {
17
19
  return located.version;
18
20
  }
19
21
  const message = `the installed @prisma/client's version could not be read (${located.reason}), so the discovery artifact's Prisma major (${String(discovery.prismaVersion).split(".")[0]}) was not compared with the installed @prisma/client's. Deploy @prisma/client where the application resolves it, or pass installedPrismaVersion to createPrismaAdapter.`;
20
- const emitWarning = globalThis.process?.emitWarning;
21
- if (typeof emitWarning === "function") {
22
- emitWarning(message, {
23
- type: "AventaraWarning",
24
- code: PRISMA_VERSION_UNCHECKED_WARNING,
25
- });
26
- }
27
- else {
28
- globalThis.console?.warn(`AventaraWarning: ${message}`);
29
- }
22
+ emitAdapterWarning(message, PRISMA_VERSION_UNCHECKED_WARNING);
30
23
  return undefined;
31
24
  }
32
25
  export async function createPrismaAdapter(options) {
@@ -36,6 +29,7 @@ export async function createPrismaAdapter(options) {
36
29
  installedPrismaVersion: installedClientVersion(options.discovery, options.installedPrismaVersion),
37
30
  });
38
31
  assertClientMatchesArtifact(options.discovery.client, options.client);
32
+ assertClientUsesDeclaredDriver(options.driver, options.client);
39
33
  const model = description.model;
40
34
  const referentialActions = description.referentialActions;
41
35
  const backReferences = derivePrismaBackReferences(options.discovery.model);
@@ -43,18 +37,38 @@ export async function createPrismaAdapter(options) {
43
37
  const computedFields = description.computedFields === undefined
44
38
  ? {}
45
39
  : { computedFields: description.computedFields };
46
- const { execute } = createPrismaOperationExecutor({
47
- client: options.client,
40
+ const translation = options.discovery.translation;
41
+ const transactionOptions = options.transactionOptions === undefined
42
+ ? {}
43
+ : { transactionOptions: options.transactionOptions };
44
+ const sharedConnection = resolveTransactionConnection(options.driver) === "shared"
45
+ ? createSharedConnectionRouter({
46
+ client: options.client,
47
+ ...transactionOptions,
48
+ })
49
+ : undefined;
50
+ const executorDependencies = {
48
51
  model: options.discovery.model,
49
- translation: options.discovery.translation,
52
+ translation,
50
53
  backReferences,
51
54
  bigintBinding,
52
- scope: "root",
53
- ...(options.transactionOptions === undefined
54
- ? {}
55
- : { transactionOptions: options.transactionOptions }),
56
- });
57
- const executeSql = createPrismaSqlStatementExecutor(options.client);
55
+ };
56
+ const execute = sharedConnection === undefined
57
+ ? createPrismaOperationExecutor({
58
+ ...executorDependencies,
59
+ client: options.client,
60
+ scope: "root",
61
+ ...transactionOptions,
62
+ }).execute
63
+ : (operation) => sharedConnection.plain(operation, (transactional) => createPrismaOperationExecutor({
64
+ ...executorDependencies,
65
+ client: transactional,
66
+ scope: "transaction",
67
+ }).execute(operation));
68
+ const rootSql = createPrismaSqlStatementExecutor(options.client);
69
+ const executeSql = sharedConnection === undefined
70
+ ? rootSql
71
+ : (statement) => sharedConnection.plain("ctx.$sql", (transactional) => createPrismaSqlStatementExecutor(transactional)(statement));
58
72
  if (description.transactions === "none") {
59
73
  return {
60
74
  model,
@@ -68,12 +82,11 @@ export async function createPrismaAdapter(options) {
68
82
  const transaction = createPrismaTransactionExecutor({
69
83
  client: options.client,
70
84
  model: options.discovery.model,
71
- translation: options.discovery.translation,
85
+ translation,
72
86
  backReferences,
73
87
  bigintBinding,
74
- ...(options.transactionOptions === undefined
75
- ? {}
76
- : { transactionOptions: options.transactionOptions }),
88
+ ...transactionOptions,
89
+ ...(sharedConnection === undefined ? {} : { sharedConnection }),
77
90
  });
78
91
  return {
79
92
  model,
@@ -35,6 +35,10 @@ export type AvPrismaProviderProfile = {
35
35
  };
36
36
  /** How a driver binds a `bigint` parameter: all 64 bits, or as a JavaScript safe integer. */
37
37
  export type PrismaBigintBinding = "int64" | "safe-integer";
38
+ /** Whether an open interactive transaction holds the connection plain statements use. */
39
+ export type PrismaTransactionConnection = "shared" | "separate";
40
+ /** Whether the declared driver's open transaction holds its plain statements' connection. */
41
+ export declare function resolveTransactionConnection(driver: AvPrismaDriver): PrismaTransactionConnection;
38
42
  /** How the declared driver binds a `bigint` parameter. */
39
43
  export declare function resolveBigintBinding(driver: AvPrismaDriver): PrismaBigintBinding;
40
44
  /**
@@ -14,6 +14,14 @@ const DRIVER_BIGINT_BINDING = {
14
14
  "@prisma/adapter-pg": "int64",
15
15
  "@prisma/adapter-d1": "safe-integer",
16
16
  };
17
+ const DRIVER_TRANSACTION_CONNECTION = {
18
+ "@prisma/adapter-better-sqlite3": "shared",
19
+ "@prisma/adapter-pg": "separate",
20
+ "@prisma/adapter-d1": "separate",
21
+ };
22
+ export function resolveTransactionConnection(driver) {
23
+ return DRIVER_TRANSACTION_CONNECTION[driver];
24
+ }
17
25
  export function resolveBigintBinding(driver) {
18
26
  return DRIVER_BIGINT_BINDING[driver];
19
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aventara/prisma7-adapter",
3
- "version": "1.0.0-rc.12",
3
+ "version": "1.0.0-rc.14",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "The Prisma 7 adapter for Aventara.",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "CHANGELOG.md"
31
31
  ],
32
32
  "dependencies": {
33
- "@aventara/core": "1.0.0-rc.12"
33
+ "@aventara/core": "1.0.0-rc.14"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@prisma/adapter-better-sqlite3": "^7.10.1",
@@ -95,9 +95,10 @@
95
95
  "build": "node ../../scripts/build/build-output.cleaner.ts dist && node node_modules/typescript/bin/tsc -p tsconfig.json --removeComments --declaration false && node node_modules/typescript/bin/tsc -p tsconfig.json --emitDeclarationOnly && node ../../scripts/build/declaration-comments.sanitizer.ts dist",
96
96
  "typecheck": "node node_modules/typescript/bin/tsc -p tsconfig.typecheck.json --noEmit",
97
97
  "generate": "node node_modules/typescript/bin/tsc -p tsconfig.generate.json && node .generate/generate/discovery.generate.js && prisma generate --schema src/discovery/__fixtures__/ignore.prisma",
98
- "db:push": "prisma db push --config prisma.config.sqlite.ts && sh -c 'if [ -n \"$AVENTARA_TEST_POSTGRES_URL\" ]; then prisma db push --config prisma.config.postgres.ts; else echo \"AVENTARA_TEST_POSTGRES_URL is unset: skipping the PostgreSQL schema push\"; fi'",
99
- "test": "pnpm run generate && pnpm run db:push && vitest run --typecheck --typecheck.checker node_modules/typescript/bin/tsc --no-file-parallelism --config vitest.config.mts --root .",
100
- "test:sqlite": "pnpm run generate && prisma db push --config prisma.config.sqlite.ts && vitest run --config vitest.config.mts --root . src/environment.spec.ts",
98
+ "db:push": "prisma db push --config prisma.config.sqlite.ts && pnpm run db:push:postgres",
99
+ "db:push:postgres": "sh -c 'if [ -n \"$AVENTARA_TEST_POSTGRES_URL\" ]; then prisma db push --config prisma.config.postgres.ts; else echo \"AVENTARA_TEST_POSTGRES_URL is unset: skipping the PostgreSQL schema push\"; fi'",
100
+ "test": "pnpm run generate && pnpm run db:push:postgres && vitest run --typecheck --typecheck.checker node_modules/typescript/bin/tsc --no-file-parallelism --config vitest.config.mts --root .",
101
+ "test:sqlite": "pnpm run generate && vitest run --config vitest.config.mts --root . src/environment.spec.ts",
101
102
  "test:postgres": "pnpm run generate && prisma db push --config prisma.config.postgres.ts && vitest run --config vitest.config.mts --root . src/environment.spec.ts",
102
103
  "lint": "biome lint src"
103
104
  }