Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/native-typeorm-isolation-levels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@nestm/crud-typeorm": minor
---

Replace `TypeOrmCrudTransactionIsolationLevel`'s lowercase string enum with a
same-name const object and derived union type whose supported values use
TypeORM's native uppercase `IsolationLevel` spelling. Member access remains
unchanged, while values can now pass directly to TypeORM transaction APIs.
6 changes: 6 additions & 0 deletions .github/scripts/consumer-fixtures/crud-typeorm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import "reflect-metadata";

import {
TypeOrmCrudAdapter,
TypeOrmCrudTransactionIsolationLevel,
bindTypeOrmCrud,
compileTypeOrmPredicate,
createTypeOrmCrudAdapter,
} from "@nestm/crud-typeorm";
import type { IsolationLevel } from "typeorm/driver/types/IsolationLevel.js";

assert.equal(typeof TypeOrmCrudAdapter, "function");
assert.equal(typeof bindTypeOrmCrud, "function");
assert.equal(typeof compileTypeOrmPredicate, "function");
assert.equal(typeof createTypeOrmCrudAdapter, "function");
const isolationLevel: IsolationLevel = TypeOrmCrudTransactionIsolationLevel.RepeatableRead;
const configuredIsolationLevel: TypeOrmCrudTransactionIsolationLevel = "READ COMMITTED";
assert.equal(isolationLevel, "REPEATABLE READ");
assert.equal(configuredIsolationLevel, TypeOrmCrudTransactionIsolationLevel.ReadCommitted);

process.stdout.write("@nestm/crud-typeorm imported from its isolated packed artifact.\n");
4 changes: 4 additions & 0 deletions packages/crud-typeorm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ const documentsAdapter = createTypeOrmCrudAdapter({
});
```

The exported members use TypeORM's native uppercase `IsolationLevel` values,
so they can be passed directly to `QueryRunner.startTransaction()` and other
TypeORM transaction APIs.

This is distinct from `rowPredicate.transaction`, which describes only the
predicate's own read/update/delete requirement. An operation-wide requirement
also applies to create hooks and validators, where a nested policy-store read
Expand Down
18 changes: 9 additions & 9 deletions packages/crud-typeorm/src/typeorm-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
type Repository,
type SelectQueryBuilder,
} from "typeorm";
import type { IsolationLevel } from "typeorm/driver/types/IsolationLevel.js";

import { compileTypeOrmPredicate } from "./typeorm-predicate.ts";

Expand Down Expand Up @@ -248,10 +249,13 @@ export const TYPEORM_CRUD_REFERENCE_ALIAS = "crud_reference";
const TYPEORM_CRUD_ADAPTER_FACTORY = Symbol("@nestm/crud-typeorm:adapter-factory");

export type TypeOrmCrudTransactionAccessMode = "read only" | "read write";
export enum TypeOrmCrudTransactionIsolationLevel {
ReadCommitted = "read committed",
RepeatableRead = "repeatable read",
}
/** TypeORM-native isolation levels supported by the CRUD transaction lifecycle. */
export const TypeOrmCrudTransactionIsolationLevel = {
ReadCommitted: "READ COMMITTED",
RepeatableRead: "REPEATABLE READ",
} as const satisfies Record<"ReadCommitted" | "RepeatableRead", IsolationLevel>;
export type TypeOrmCrudTransactionIsolationLevel =
(typeof TypeOrmCrudTransactionIsolationLevel)[keyof typeof TypeOrmCrudTransactionIsolationLevel];

export interface TypeOrmCrudTransactionRequirements {
readonly accessMode: TypeOrmCrudTransactionAccessMode;
Expand Down Expand Up @@ -1279,11 +1283,7 @@ export class TypeOrmCrudAdapter<
): Promise<Result> {
const queryRunner = this.#repository.manager.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction(
requirements.isolationLevel === TypeOrmCrudTransactionIsolationLevel.RepeatableRead
? "REPEATABLE READ"
: "READ COMMITTED",
);
await queryRunner.startTransaction(requirements.isolationLevel);
try {
if (requirements.accessMode === "read only") {
await queryRunner.query("SET TRANSACTION READ ONLY");
Expand Down
4 changes: 2 additions & 2 deletions packages/crud-typeorm/tests/typeorm-adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ describe("TypeOrmCrudAdapter row predicate", () => {
expect(runner.contexts[0]).toMatchObject({
operation: "create",
accessMode: "read write",
isolationLevel: "repeatable read",
isolationLevel: "REPEATABLE READ",
mustOwnCommit: true,
});
});
Expand Down Expand Up @@ -546,7 +546,7 @@ describe("TypeOrmCrudAdapter transaction runner", () => {
expect(runner.contexts[1]).toMatchObject({
operation: "list",
accessMode: "read only",
isolationLevel: "read committed",
isolationLevel: "READ COMMITTED",
mustOwnCommit: false,
});
// The runner owns the transaction; the adapter must not open its own.
Expand Down
11 changes: 10 additions & 1 deletion packages/crud-typeorm/tests/typeorm-types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { crudOperations, defineCrudResource } from "@nestm/crud";
import type { CrudAdapter, CrudAdapterSession } from "@nestm/crud/adapter";
import { Brackets, type DeepPartial, type Repository } from "typeorm";
import type { IsolationLevel } from "typeorm/driver/types/IsolationLevel.js";
import { z } from "zod";

import {
Expand All @@ -14,6 +15,14 @@
type TypeOrmCrudSelectedRecord,
} from "../src/index.ts";

const nativeIsolationLevel: IsolationLevel = TypeOrmCrudTransactionIsolationLevel.RepeatableRead;
const configuredIsolationLevel: TypeOrmCrudTransactionIsolationLevel = "READ COMMITTED";
// @ts-expect-error The CRUD lifecycle deliberately supports only its documented subset.
const unsupportedIsolationLevel: TypeOrmCrudTransactionIsolationLevel = "SERIALIZABLE";
void nativeIsolationLevel;
void configuredIsolationLevel;
void unsupportedIsolationLevel;

class UserProfile {
readonly nickname!: string;
}
Expand Down Expand Up @@ -129,7 +138,7 @@
repository,
columns: { id: "id" },
transaction: {
// @ts-expect-error Isolation is intentionally an enum, not a free-form string.
// @ts-expect-error TypeORM isolation values use their native uppercase spelling.
isolationLevel: "repeatable read",
},
});
Expand Down Expand Up @@ -173,7 +182,7 @@
// The public class constructor is full-entity only; selected construction goes through
// the factory so its narrowed output type cannot be accidentally omitted.
// @ts-expect-error Direct construction cannot accept a selected record configuration.
new TypeOrmCrudAdapter({ repository, columns: { id: "id" }, select: { id: true } });

Check warning on line 185 in packages/crud-typeorm/tests/typeorm-types.ts

View workflow job for this annotation

GitHub Actions / check

eslint(no-new)

packages/crud-typeorm/tests/typeorm-types.ts:185:1: Do not use 'new' for side effects.

declare const widenedOptions: TypeOrmCrudAdapterOptions<UserEntity>;
const widenedAdapter: CrudAdapter<
Expand Down
14 changes: 3 additions & 11 deletions tests/postgres/adapters.postgres.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,7 @@ async function initializeHarnesses(pgUrl: string): Promise<void> {
// issue `SET TRANSACTION READ ONLY`, and it has to be the first statement.
const queryRunner = typeOrmDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction(
runnerContext.isolationLevel === TypeOrmCrudTransactionIsolationLevel.RepeatableRead
? "REPEATABLE READ"
: "READ COMMITTED",
);
await queryRunner.startTransaction(runnerContext.isolationLevel);
try {
if (runnerContext.accessMode === "read only") {
await queryRunner.query("SET TRANSACTION READ ONLY");
Expand Down Expand Up @@ -346,11 +342,7 @@ async function initializeHarnesses(pgUrl: string): Promise<void> {
run: async (runnerContext, workWithTransaction) => {
const queryRunner = typeOrmDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction(
runnerContext.isolationLevel === TypeOrmCrudTransactionIsolationLevel.RepeatableRead
? "REPEATABLE READ"
: "READ COMMITTED",
);
await queryRunner.startTransaction(runnerContext.isolationLevel);
try {
const rows: unknown = await queryRunner.query("SHOW transaction_isolation");
const first = Array.isArray(rows) ? rows[0] : undefined;
Expand Down Expand Up @@ -1138,7 +1130,7 @@ describe.skipIf(skipPostgres)("PostgreSQL adapter conformance", () => {
expect(runnerContexts).toHaveLength(3);
expect(runnerContexts[0]).toMatchObject({
accessMode: "read only",
isolationLevel: "repeatable read",
isolationLevel: "REPEATABLE READ",
mustOwnCommit: false,
});
expect(runnerContexts[1]).toMatchObject({ mustOwnCommit: true });
Expand Down
Loading