Skip to content

Commit 8c2a9bf

Browse files
committed
vite beta 18 rolldown rc 8.
2 parents e41a4a5 + c5171c3 commit 8c2a9bf

27 files changed

Lines changed: 1755 additions & 327 deletions

CHANGELOG.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,37 @@
22

33
## Version 27
44

5+
### v27.1.0
6+
7+
- Introducing `ez.paginated()` helper for creating paginated endpoints:
8+
- The configurable helper returns `input` and `output` schemas for your Endpoint;
9+
- Use the `style` option to choose between `offset` and `cursor` pagination;
10+
- The `itemsName` option configures the name of the property containing the items array;
11+
- The `Integration` generator now equips the `Client` with a static `hasMore()` method;
12+
- The `Incremental` added to the list of well-known recognizable request headers.
13+
14+
```ts
15+
import { z } from "zod";
16+
import { ez, defaultEndpointsFactory } from "express-zod-api";
17+
18+
const pagination = ez.paginated({
19+
style: "offset", // or "cursor"
20+
itemSchema: z.object({ id: z.number(), name: z.string() }),
21+
itemsName: "users", // defines the output name of the items array
22+
maxLimit: 100,
23+
defaultLimit: 20,
24+
});
25+
26+
const listUsers = defaultEndpointsFactory.build({
27+
input: pagination.input,
28+
output: pagination.output,
29+
handler: async ({ input: { limit, offset } }) => {
30+
const { users, total } = await db.getUsers(limit, offset);
31+
return { users, total, limit, offset }; // or { users, nextCursor, limit } for cursor pagination
32+
},
33+
});
34+
```
35+
536
### v27.0.1
637

738
- Removed debug-level comments from the declaration files in the distribution.
@@ -2186,7 +2217,7 @@ const labeledDefaultSchema = withMeta(
21862217
capabilities, so that the user could subscribe to subsequent updates initiated by the server;
21872218
- Check out an [example of the synergy between two frameworks](https://github.com/RobinTail/zod-sockets#subscriptions)
21882219
and the [Demo Chat application](https://github.com/RobinTail/chat);
2189-
- The feature suggested by [@ben-xD](https://github.com/ben-xD).
2220+
- The feature suggested by [@uxduck](https://github.com/uxduck).
21902221

21912222
### v18.1.0
21922223

README.md

Lines changed: 86 additions & 59 deletions
Large diffs are not rendered by default.

dts-plugin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"build": "tsc"
1212
},
1313
"devDependencies": {
14-
"rolldown": "^1.0.0-rc.3",
14+
"rolldown": "^1.0.0-rc.7",
1515
"typescript": "catalog:dev"
1616
}
1717
}

example/__snapshots__/index.spec.ts.snap

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,51 @@ exports[`Example > Positive > Should respond with array (legacy API ResultHandle
173173
]
174174
`;
175175

176+
exports[`Example > Positive > Should respond with paginated list (ez.paginated) 1`] = `
177+
{
178+
"data": {
179+
"limit": 20,
180+
"offset": 0,
181+
"total": 8,
182+
"users": [
183+
{
184+
"name": "Maria Merian",
185+
"role": "manager",
186+
},
187+
{
188+
"name": "Mary Anning",
189+
"role": "operator",
190+
},
191+
{
192+
"name": "Marie Skłodowska Curie",
193+
"role": "admin",
194+
},
195+
{
196+
"name": "Henrietta Leavitt",
197+
"role": "manager",
198+
},
199+
{
200+
"name": "Lise Meitner",
201+
"role": "operator",
202+
},
203+
{
204+
"name": "Alice Ball",
205+
"role": "admin",
206+
},
207+
{
208+
"name": "Gerty Cori",
209+
"role": "manager",
210+
},
211+
{
212+
"name": "Helen Taussig",
213+
"role": "operator",
214+
},
215+
],
216+
},
217+
"status": "success",
218+
}
219+
`;
220+
176221
exports[`Example > Positive > Should send an image with a correct header 1`] = `"f39beeff92379dc935586d726211c2620be6f879"`;
177222

178223
exports[`Example > Positive > Should serve static files 1`] = `"f39beeff92379dc935586d726211c2620be6f879"`;

example/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import qs from "qs";
66

77
export const config = createConfig({
88
http: { listen: givePort("example") },
9-
queryParser: (query) => qs.parse(query, { comma: true }), // affects listUsersEndpoint
9+
// required for listUsersPaginatedEndpoint and listUsersEndpoint (legacy API demo)
10+
queryParser: (query) => qs.parse(query, { comma: true }),
1011
upload: {
1112
limits: { fileSize: 51200 },
1213
limitError: createHttpError(413, "The file is too large"), // affects uploadAvatarEndpoint
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { z } from "zod";
2+
import { defaultEndpointsFactory, ez } from "express-zod-api";
3+
4+
const roleSchema = z.enum(["manager", "operator", "admin"]);
5+
6+
const userSchema = z.object({
7+
name: z.string(),
8+
role: roleSchema,
9+
});
10+
11+
const paginatedUsers = ez.paginated({
12+
style: "offset",
13+
itemSchema: userSchema,
14+
itemsName: "users",
15+
maxLimit: 100,
16+
defaultLimit: 20,
17+
});
18+
19+
const users: z.output<typeof userSchema>[] = [
20+
{ name: "Maria Merian", role: "manager" },
21+
{ name: "Mary Anning", role: "operator" },
22+
{ name: "Marie Skłodowska Curie", role: "admin" },
23+
{ name: "Henrietta Leavitt", role: "manager" },
24+
{ name: "Lise Meitner", role: "operator" },
25+
{ name: "Alice Ball", role: "admin" },
26+
{ name: "Gerty Cori", role: "manager" },
27+
{ name: "Helen Taussig", role: "operator" },
28+
];
29+
30+
/**
31+
* Lists users with offset pagination and optional role filter.
32+
* Uses ez.paginated() for input params (limit, offset) and output shape (users, total, limit, offset).
33+
*/
34+
export const listUsersPaginatedEndpoint = defaultEndpointsFactory.build({
35+
tag: "users",
36+
shortDescription: "Lists users with pagination.",
37+
description:
38+
"Returns a page of users. Optionally filter by roles. Uses offset-based pagination (limit and offset).",
39+
input: paginatedUsers.input.and(
40+
z.object({
41+
roles: z
42+
.array(roleSchema)
43+
.optional()
44+
.describe("Filter by roles; omit for all"),
45+
}),
46+
),
47+
output: paginatedUsers.output,
48+
handler: async ({ input: { limit, offset, roles } }) => {
49+
const filtered = roles
50+
? users.filter(({ role }) => roles.includes(role))
51+
: users;
52+
const total = filtered.length;
53+
const page = filtered.slice(offset, offset + limit);
54+
return { users: page, total, limit, offset };
55+
},
56+
});

example/example.client.ts

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,78 @@ interface PostV1FormsFeedbackNegativeResponseVariants {
435435
400: PostV1FormsFeedbackNegativeVariant1;
436436
}
437437

438+
/** get /v2/users/list */
439+
type GetV2UsersListInput = {
440+
/** Page size (number of users per page) */
441+
limit?: number | undefined;
442+
/** Number of users to skip */
443+
offset?: number | undefined;
444+
/** Filter by roles; omit for all */
445+
roles?: ("manager" | "operator" | "admin")[] | undefined;
446+
};
447+
448+
/** get /v2/users/list */
449+
type GetV2UsersListPositiveVariant1 = {
450+
status: "success";
451+
data: {
452+
/** Page of users */
453+
users: {
454+
name: string;
455+
role: "manager" | "operator" | "admin";
456+
}[];
457+
/** Total number of users */
458+
total: number;
459+
/** Page size used */
460+
limit: number;
461+
/** Offset used */
462+
offset: number;
463+
};
464+
};
465+
466+
/** get /v2/users/list */
467+
interface GetV2UsersListPositiveResponseVariants {
468+
200: GetV2UsersListPositiveVariant1;
469+
}
470+
471+
/** get /v2/users/list */
472+
type GetV2UsersListNegativeVariant1 = {
473+
status: "error";
474+
error: {
475+
message: string;
476+
};
477+
};
478+
479+
/** get /v2/users/list */
480+
interface GetV2UsersListNegativeResponseVariants {
481+
400: GetV2UsersListNegativeVariant1;
482+
}
483+
484+
/** head /v2/users/list */
485+
type HeadV2UsersListInput = {
486+
/** Page size (number of users per page) */
487+
limit?: number | undefined;
488+
/** Number of users to skip */
489+
offset?: number | undefined;
490+
/** Filter by roles; omit for all */
491+
roles?: ("manager" | "operator" | "admin")[] | undefined;
492+
};
493+
494+
/** head /v2/users/list */
495+
type HeadV2UsersListPositiveVariant1 = undefined;
496+
497+
/** head /v2/users/list */
498+
interface HeadV2UsersListPositiveResponseVariants {
499+
200: HeadV2UsersListPositiveVariant1;
500+
}
501+
502+
/** head /v2/users/list */
503+
type HeadV2UsersListNegativeVariant1 = undefined;
504+
505+
/** head /v2/users/list */
506+
interface HeadV2UsersListNegativeResponseVariants {
507+
400: HeadV2UsersListNegativeVariant1;
508+
}
509+
438510
export type Path =
439511
| "/v1/user/retrieve"
440512
| "/v1/user/:id/remove"
@@ -446,7 +518,8 @@ export type Path =
446518
| "/v1/avatar/upload"
447519
| "/v1/avatar/raw"
448520
| "/v1/events/stream"
449-
| "/v1/forms/feedback";
521+
| "/v1/forms/feedback"
522+
| "/v2/users/list";
450523

451524
export type Method = "get" | "post" | "put" | "delete" | "patch" | "head";
452525

@@ -469,6 +542,8 @@ export interface Input {
469542
"get /v1/events/stream": GetV1EventsStreamInput;
470543
"head /v1/events/stream": HeadV1EventsStreamInput;
471544
"post /v1/forms/feedback": PostV1FormsFeedbackInput;
545+
"get /v2/users/list": GetV2UsersListInput;
546+
"head /v2/users/list": HeadV2UsersListInput;
472547
}
473548

474549
export interface PositiveResponse {
@@ -490,6 +565,8 @@ export interface PositiveResponse {
490565
"get /v1/events/stream": SomeOf<GetV1EventsStreamPositiveResponseVariants>;
491566
"head /v1/events/stream": SomeOf<HeadV1EventsStreamPositiveResponseVariants>;
492567
"post /v1/forms/feedback": SomeOf<PostV1FormsFeedbackPositiveResponseVariants>;
568+
"get /v2/users/list": SomeOf<GetV2UsersListPositiveResponseVariants>;
569+
"head /v2/users/list": SomeOf<HeadV2UsersListPositiveResponseVariants>;
493570
}
494571

495572
export interface NegativeResponse {
@@ -511,6 +588,8 @@ export interface NegativeResponse {
511588
"get /v1/events/stream": SomeOf<GetV1EventsStreamNegativeResponseVariants>;
512589
"head /v1/events/stream": SomeOf<HeadV1EventsStreamNegativeResponseVariants>;
513590
"post /v1/forms/feedback": SomeOf<PostV1FormsFeedbackNegativeResponseVariants>;
591+
"get /v2/users/list": SomeOf<GetV2UsersListNegativeResponseVariants>;
592+
"head /v2/users/list": SomeOf<HeadV2UsersListNegativeResponseVariants>;
514593
}
515594

516595
export interface EncodedResponse {
@@ -548,6 +627,10 @@ export interface EncodedResponse {
548627
HeadV1EventsStreamNegativeResponseVariants;
549628
"post /v1/forms/feedback": PostV1FormsFeedbackPositiveResponseVariants &
550629
PostV1FormsFeedbackNegativeResponseVariants;
630+
"get /v2/users/list": GetV2UsersListPositiveResponseVariants &
631+
GetV2UsersListNegativeResponseVariants;
632+
"head /v2/users/list": HeadV2UsersListPositiveResponseVariants &
633+
HeadV2UsersListNegativeResponseVariants;
551634
}
552635

553636
export interface Response {
@@ -601,6 +684,12 @@ export interface Response {
601684
"post /v1/forms/feedback":
602685
| PositiveResponse["post /v1/forms/feedback"]
603686
| NegativeResponse["post /v1/forms/feedback"];
687+
"get /v2/users/list":
688+
| PositiveResponse["get /v2/users/list"]
689+
| NegativeResponse["get /v2/users/list"];
690+
"head /v2/users/list":
691+
| PositiveResponse["head /v2/users/list"]
692+
| NegativeResponse["head /v2/users/list"];
604693
}
605694

606695
export type Request = keyof Input;
@@ -622,6 +711,8 @@ export const endpointTags = {
622711
"get /v1/events/stream": ["subscriptions"],
623712
"head /v1/events/stream": ["subscriptions"],
624713
"post /v1/forms/feedback": ["forms"],
714+
"get /v2/users/list": ["users"],
715+
"head /v2/users/list": ["users"],
625716
};
626717

627718
const parseRequest = (request: string) =>
@@ -645,6 +736,16 @@ export type Implementation<T = unknown> = (
645736
ctx?: T,
646737
) => Promise<any>;
647738

739+
type Pagination =
740+
| {
741+
nextCursor: string | null;
742+
}
743+
| {
744+
total: number;
745+
limit: number;
746+
offset: number;
747+
};
748+
648749
const defaultImplementation: Implementation = async (method, path, params) => {
649750
const hasBody = !["get", "head", "delete"].includes(method);
650751
const searchParams = hasBody ? "" : `?${new URLSearchParams(params)}`;
@@ -674,6 +775,10 @@ export class Client<T> {
674775
const [method, path] = parseRequest(request);
675776
return this.implementation(method, ...substitute(path, params), ctx);
676777
}
778+
public static hasMore(response: Pagination): boolean {
779+
if ("nextCursor" in response) return response.nextCursor !== null;
780+
return response.offset + response.limit < response.total;
781+
}
677782
}
678783

679784
export class Subscription<

0 commit comments

Comments
 (0)