Skip to content

Commit 57668cc

Browse files
committed
feat: support custom Prisma client output paths
1 parent 5dab9a8 commit 57668cc

File tree

4 files changed

+132
-13
lines changed

4 files changed

+132
-13
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,27 @@ const client = new PrismaClient();
3030
client.$use(stableSortMiddleware(client));
3131
```
3232

33+
### Usage with a custom Prisma Client output path
34+
35+
If your Prisma schema generates the client to a custom location, import your generated client from that location and pass it to the middleware as usual.
36+
37+
```prisma
38+
generator client {
39+
provider = "prisma-client-js"
40+
output = "../src/generated/prisma"
41+
}
42+
```
43+
44+
```ts
45+
import { stableSortMiddleware } from "@cerebruminc/prisma-stable-sort-middleware";
46+
import { PrismaClient } from "../src/generated/prisma";
47+
48+
const client = new PrismaClient();
49+
client.$use(stableSortMiddleware(client));
50+
```
51+
52+
This package does not import `@prisma/client/runtime/library`; it only requires a generated Prisma client instance that exposes Prisma's runtime data model.
53+
3354
## License
3455

3556
The project is licensed under the MIT license.
@@ -43,3 +64,12 @@ The project is licensed under the MIT license.
4364
![Cerebrum](./images/powered-by-cerebrum-dm.svg#gh-dark-mode-only)
4465

4566
</div>
67+
68+
## Contributing
69+
70+
Before opening a pull request, please run:
71+
72+
- `npm test`
73+
- `npm run test:integration`
74+
75+
Please also follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification when writing commit messages.

docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ services:
2626
POSTGRES_PASSWORD: postgres
2727
POSTGRES_DB: yates
2828
ports:
29-
- 5432:5432
29+
- 55432:5432
3030
networks:
3131
- internal
3232
healthcheck:
3333
test: ["CMD-SHELL", "pg_isready -U postgres"]
3434
interval: 5s
3535
timeout: 5s
3636
retries: 5
37-
# command: ["postgres", "-c", "log_statement=all", "-c", "log_destination=stderr"]
37+
# command: ["postgres", "-c", "log_statement=all", "-c", "log_destination=stderr"]

src/index.ts

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,48 @@
1-
import type { Prisma, PrismaClient } from "@prisma/client";
2-
import { defineDmmfProperty } from "@prisma/client/runtime/library";
1+
export interface RuntimeFieldLike {
2+
name: string;
3+
isId: boolean;
4+
isUnique: boolean;
5+
}
36

4-
// RuntimeDataModel is not exported from Prisma, so we need to use some type infiltration to get it
5-
export type RuntimeDataModel = Parameters<typeof defineDmmfProperty>[1];
7+
export interface RuntimeModelLike {
8+
fields: RuntimeFieldLike[];
9+
}
10+
11+
export interface RuntimeDataModelLike {
12+
models: { [model: string]: RuntimeModelLike };
13+
}
14+
15+
export type RuntimeDataModel = RuntimeDataModelLike;
16+
17+
export type PrismaClientLike = object & { _runtimeDataModel?: RuntimeDataModelLike };
18+
19+
export interface MiddlewareParamsLike {
20+
model?: string;
21+
action?: string;
22+
args?: {
23+
orderBy?: Record<string, unknown> | Array<Record<string, unknown>>;
24+
[key: string]: unknown;
25+
};
26+
dataPath?: string[];
27+
runInTransaction?: boolean;
28+
[key: string]: unknown;
29+
}
30+
31+
type MiddlewareNextLike = (params: any) => Promise<any> | any;
32+
33+
export type MiddlewareLike = (params: any, next: MiddlewareNextLike) => Promise<any> | any;
34+
35+
const getRuntimeDataModel = (client: PrismaClientLike): RuntimeDataModelLike => {
36+
const runtimeDataModel = client._runtimeDataModel;
37+
38+
if (!(runtimeDataModel?.models && typeof runtimeDataModel.models === "object")) {
39+
throw new Error(
40+
`Could not access Prisma runtime data model on the provided client. Ensure you pass a generated Prisma client instance that exposes "_runtimeDataModel".`,
41+
);
42+
}
43+
44+
return runtimeDataModel;
45+
};
646

747
const castArray = <T>(value: T | T[]): T[] => {
848
if (Array.isArray(value)) {
@@ -12,9 +52,9 @@ const castArray = <T>(value: T | T[]): T[] => {
1252
};
1353

1454
// Creates an object indicating the unique fields for each model
15-
const getUniqueFieldData = (client: PrismaClient) => {
55+
const getUniqueFieldData = (client: PrismaClientLike) => {
1656
const uniqueFields: { [model: string]: { [field: string]: boolean } } = {};
17-
const runtimeDataModel = (client as any)._runtimeDataModel as RuntimeDataModel;
57+
const runtimeDataModel = getRuntimeDataModel(client);
1858
for (const key in runtimeDataModel.models) {
1959
if (!uniqueFields[key]) {
2060
uniqueFields[key] = {};
@@ -30,20 +70,24 @@ const getUniqueFieldData = (client: PrismaClient) => {
3070
// If the orderBy is just createdAt, add an id desc to the end of the orderBy
3171
// The unique ID stabilizes the sort, providing consistent ordering when the createdAt is the same
3272
// This is needed because the createdAt is not unique, and batch uploads will have the same createdAt
33-
export const stableSortMiddleware = (client: PrismaClient): Prisma.Middleware => {
73+
export const stableSortMiddleware = (client: PrismaClientLike): MiddlewareLike => {
3474
const uniqueFields = getUniqueFieldData(client);
3575

36-
return (params, next) => {
76+
return (params: MiddlewareParamsLike, next: MiddlewareNextLike) => {
3777
const { model, action } = params;
3878

3979
if (!(model && action)) {
4080
return next(params);
4181
}
82+
if (!uniqueFields[model]) {
83+
return next(params);
84+
}
4285

86+
const args = params.args;
4387
// In Prisma, sorting by a single field can be done by passing a plain object, so for convenience we cast it to an array
44-
const orderBy = params?.args?.orderBy ? castArray(params.args.orderBy) : null;
88+
const orderBy = args?.orderBy ? castArray(args.orderBy) : null;
4589

46-
if (orderBy) {
90+
if (orderBy && args) {
4791
const hasUniqueField = orderBy.some((o) => {
4892
const [field] = Object.keys(o);
4993
return uniqueFields[model][field];
@@ -65,7 +109,7 @@ export const stableSortMiddleware = (client: PrismaClient): Prisma.Middleware =>
65109
orderBy.push({ [fieldName]: "desc" });
66110
}
67111
}
68-
params.args.orderBy = orderBy;
112+
args.orderBy = orderBy;
69113
}
70114
}
71115
return next(params);

test/unit/index.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { stableSortMiddleware, type MiddlewareParamsLike, type PrismaClientLike } from "../../src";
2+
3+
describe("stableSortMiddleware", () => {
4+
it("throws a clear error when the runtime data model is missing", () => {
5+
expect(() => stableSortMiddleware({})).toThrow(
6+
'Could not access Prisma runtime data model on the provided client. Ensure you pass a generated Prisma client instance that exposes "_runtimeDataModel".',
7+
);
8+
});
9+
10+
it("adds a unique id field to stabilize sorting when orderBy is not unique", async () => {
11+
const client: PrismaClientLike = {
12+
_runtimeDataModel: {
13+
models: {
14+
User: {
15+
fields: [
16+
{ name: "id", isId: true, isUnique: false },
17+
{ name: "createdAt", isId: false, isUnique: false },
18+
{ name: "email", isId: false, isUnique: true },
19+
],
20+
},
21+
},
22+
},
23+
};
24+
25+
const middleware = stableSortMiddleware(client);
26+
const params: MiddlewareParamsLike = {
27+
model: "User",
28+
action: "findMany",
29+
args: {
30+
orderBy: {
31+
createdAt: "desc",
32+
},
33+
},
34+
dataPath: [],
35+
runInTransaction: false,
36+
};
37+
38+
const next = jest.fn(async (nextParams: MiddlewareParamsLike) => nextParams);
39+
40+
await middleware(params, next);
41+
42+
expect(params.args?.orderBy).toEqual([{ createdAt: "desc" }, { id: "desc" }]);
43+
expect(next).toHaveBeenCalledWith(params);
44+
});
45+
});

0 commit comments

Comments
 (0)