Skip to content

Commit 63ee107

Browse files
authored
fix: types when paginationField is in body (#3635)
1 parent 57eb192 commit 63ee107

File tree

6 files changed

+326
-19
lines changed

6 files changed

+326
-19
lines changed

.changeset/lucky-paws-jog.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@data-client/rest': patch
3+
---
4+
5+
Fix `getPage` types when paginationField is in body
6+
7+
```ts
8+
const ep = new RestEndpoint({
9+
path: '/rpc',
10+
method: 'POST',
11+
body: {} as { page?: number; method: string },
12+
paginationField: 'page',
13+
});
14+
// Before: ep.getPage({ page: 2 }, { method: 'get' }) ❌
15+
// After: ep.getPage({ page: 2, method: 'get' }) ✓
16+
```

packages/rest/src/RestEndpointTypes.ts

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -376,21 +376,70 @@ export type PaginationEndpoint<
376376
searchParams: Omit<A[0], keyof PathArgs<E['path']>>;
377377
}
378378
>;
379-
export type PaginationFieldEndpoint<
379+
/** Merge pagination field C into body, making it required */
380+
type PaginationIntoBody<Body, C extends string> = Body & {
381+
[K in C]: string | number | boolean;
382+
};
383+
384+
/** Paginated searchParams type */
385+
type PaginatedSearchParams<
386+
E extends { searchParams?: any; path?: string },
387+
C extends string,
388+
> = {
389+
[K in C]: string | number | boolean;
390+
} & E['searchParams'] &
391+
PathArgs<Exclude<E['path'], undefined>>;
392+
393+
/** searchParams version: pagination in searchParams, optional body support */
394+
type PaginationFieldInSearchParams<
395+
E extends FetchFunction & RestGenerics & { sideEffect?: boolean | undefined },
396+
C extends string,
397+
> = RestInstanceBase<
398+
// Union allows calling with just searchParams or with searchParams + body
399+
| ParamFetchNoBody<PaginatedSearchParams<E, C>, ResolveType<E>>
400+
| ParamFetchWithBody<
401+
PaginatedSearchParams<E, C>,
402+
NonNullable<E['body']>,
403+
ResolveType<E>
404+
>,
405+
E['schema'],
406+
E['sideEffect'],
407+
Pick<E, 'path' | 'searchParams' | 'body'> & {
408+
searchParams: {
409+
[K in C]: string | number | boolean;
410+
} & E['searchParams'];
411+
}
412+
> & { paginationField: C };
413+
414+
/** body version: pagination field is in body (body required) */
415+
type PaginationFieldInBody<
380416
E extends FetchFunction & RestGenerics & { sideEffect?: boolean | undefined },
381417
C extends string,
382418
> = RestInstanceBase<
383-
ParamFetchNoBody<
384-
{ [K in C]: string | number | boolean } & E['searchParams'] &
385-
PathArgs<Exclude<E['path'], undefined>>,
419+
ParamFetchWithBody<
420+
E['searchParams'] & PathArgs<Exclude<E['path'], undefined>>,
421+
PaginationIntoBody<E['body'], C>,
386422
ResolveType<E>
387423
>,
388424
E['schema'],
389425
E['sideEffect'],
390-
Pick<E, 'path' | 'searchParams' | 'body'> & {
391-
searchParams: { [K in C]: string | number | boolean } & E['searchParams'];
426+
Pick<E, 'path' | 'searchParams'> & {
427+
body: PaginationIntoBody<E['body'], C>;
392428
}
393429
> & { paginationField: C };
430+
431+
/** Retrieves the next page of results by pagination field */
432+
export type PaginationFieldEndpoint<
433+
E extends FetchFunction & RestGenerics & { sideEffect?: boolean | undefined },
434+
C extends string,
435+
> =
436+
// If body can be undefined or pagination field not in body, use searchParams
437+
undefined extends E['body'] ? PaginationFieldInSearchParams<E, C>
438+
: // If pagination field C is a key of body, merge into body
439+
C extends keyof E['body'] ? PaginationFieldInBody<E, C>
440+
: // Otherwise use searchParams
441+
PaginationFieldInSearchParams<E, C>;
442+
394443
export type AddEndpoint<
395444
F extends FetchFunction = FetchFunction,
396445
S extends Schema | undefined = any,

packages/rest/typescript-tests/types.test.ts

Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
/* eslint-disable @typescript-eslint/ban-ts-comment */
21
import { Entity, schema } from '@data-client/endpoint';
32
import { useController, useSuspense } from '@data-client/react';
4-
import { User } from '__tests__/new';
3+
import { User, Article } from '__tests__/new';
54

65
import resource from '../src/resource';
76
import RestEndpoint, { GetEndpoint, MutateEndpoint } from '../src/RestEndpoint';
@@ -424,6 +423,172 @@ it('should precisely type function arguments', () => {
424423
};
425424
});
426425

426+
it('should handle POST getter endpoints', () => {
427+
() => {
428+
const getUsers = new RestEndpoint({
429+
path: '/users',
430+
method: 'POST',
431+
sideEffect: false,
432+
body: {} as {
433+
page?: number;
434+
jsonrpc: string;
435+
id: number;
436+
method: string;
437+
params: any[];
438+
},
439+
paginationField: 'page',
440+
schema: new schema.Collection([User]),
441+
});
442+
getUsers({ jsonrpc: '2.0', id: 1, method: 'users.get', params: [] });
443+
useSuspense(getUsers, {
444+
jsonrpc: '2.0',
445+
id: 1,
446+
method: 'users.get',
447+
params: [],
448+
});
449+
// @ts-expect-error
450+
getEth({ id: 1, method: 'users.get', params: [] });
451+
// @ts-expect-error
452+
useSuspense(getUsers, { id: 1, method: 'users.get', params: [] });
453+
454+
// getPage tests
455+
getUsers.getPage({
456+
page: 2,
457+
jsonrpc: '2.0',
458+
id: 1,
459+
method: 'users.get',
460+
params: [],
461+
});
462+
getUsers.getPage({
463+
// @ts-expect-error page is not a number | string
464+
page: { bob: 'hi' },
465+
jsonrpc: '2.0',
466+
id: 1,
467+
method: 'users.get',
468+
params: [],
469+
});
470+
getUsers.getPage({
471+
// @ts-expect-error page is not a number | string
472+
page: undefined,
473+
jsonrpc: '2.0',
474+
id: 1,
475+
method: 'users.get',
476+
params: [],
477+
});
478+
// @ts-expect-error
479+
getUsers.getPage({
480+
jsonrpc: '2.0',
481+
id: 1,
482+
method: 'users.get',
483+
params: [],
484+
});
485+
// @ts-expect-error
486+
getUsers.getPage({
487+
page: 2,
488+
id: 1,
489+
method: 'users.get',
490+
params: [],
491+
});
492+
getUsers.getPage(
493+
{ page: 2 },
494+
// @ts-expect-error
495+
{ jsonrpc: '2.0', id: 1, method: 'users.get', params: [] },
496+
);
497+
};
498+
() => {
499+
const getArticles = new RestEndpoint({
500+
path: '/articles',
501+
method: 'POST',
502+
sideEffect: false,
503+
searchParams: {} as {
504+
page_number?: number;
505+
groupId?: string | number;
506+
},
507+
body: {} as
508+
| {
509+
authorId?: string | number;
510+
createdAt?: string;
511+
}
512+
| undefined,
513+
schema: new schema.Collection([Article]),
514+
paginationField: 'page_number',
515+
});
516+
getArticles({ authorId: 1, createdAt: '2025-01-01' });
517+
useSuspense(getArticles, { authorId: 1, createdAt: '2025-01-01' });
518+
// @ts-expect-error
519+
getArticles({ authorId: 1, createdAt: '2025-01-01', page: 2 });
520+
// @ts-expect-error
521+
useSuspense(getArticles, { authorId: 1, createdAt: '2025-01-01', page: 2 });
522+
getArticles();
523+
getArticles({ page_number: 2 });
524+
getArticles({ page_number: 2 }, { authorId: 5 });
525+
526+
getArticles.getPage({ page_number: 2 });
527+
getArticles.getPage({ page_number: 2, groupId: 5 });
528+
getArticles.getPage({ page_number: 2 }, { authorId: 5 });
529+
getArticles.getPage(
530+
{ page_number: 2 },
531+
{ authorId: 5, createdAt: '2025-01-01' },
532+
);
533+
534+
// @ts-expect-error requires page_number
535+
getArticles.getPage();
536+
// @ts-expect-error requires page_number
537+
getArticles.getPage({});
538+
// @ts-expect-error requires page_number
539+
getArticles.getPage({ groupId: 5 });
540+
// @ts-expect-error page_number is not a valid search param
541+
getArticles.getPage({ page: 5 });
542+
// @ts-expect-error page is not page_number
543+
getArticles.getPage({ page: 5 });
544+
// @ts-expect-error adsdf is not a valid search param
545+
getArticles.getPage({ page_number: 2, adsdf: 5 });
546+
};
547+
() => {
548+
const getArticles = new RestEndpoint({
549+
path: '/articles/:groupId?',
550+
method: 'POST',
551+
sideEffect: false,
552+
body: {} as
553+
| {
554+
authorId?: string | number;
555+
createdAt?: string;
556+
}
557+
| undefined,
558+
schema: new schema.Collection([Article]),
559+
paginationField: 'page_number',
560+
});
561+
getArticles({ authorId: 1, createdAt: '2025-01-01' });
562+
useSuspense(getArticles, { authorId: 1, createdAt: '2025-01-01' });
563+
// @ts-expect-error
564+
getArticles({ authorId: 1, createdAt: '2025-01-01', page: 2 });
565+
// @ts-expect-error
566+
useSuspense(getArticles, { authorId: 1, createdAt: '2025-01-01', page: 2 });
567+
getArticles();
568+
569+
getArticles.getPage({ page_number: 2 });
570+
getArticles.getPage({ page_number: 2, groupId: 5 });
571+
getArticles.getPage({ page_number: 2 }, { authorId: 5 });
572+
getArticles.getPage(
573+
{ page_number: 2 },
574+
{ authorId: 5, createdAt: '2025-01-01' },
575+
);
576+
577+
// @ts-expect-error requires page_number
578+
getArticles.getPage();
579+
// @ts-expect-error requires page_number
580+
getArticles.getPage({});
581+
// @ts-expect-error requires page_number
582+
getArticles.getPage({ groupId: 5 });
583+
// @ts-expect-error page_number is not a valid search param
584+
getArticles.getPage({ page: 5 });
585+
// @ts-expect-error page is not page_number
586+
getArticles.getPage({ page: 5 });
587+
// @ts-expect-error adsdf is not a valid search param
588+
getArticles.getPage({ page_number: 2, adsdf: 5 });
589+
};
590+
});
591+
427592
it('should allow sideEffect overrides', () => {
428593
() => {
429594
const getEth = new RestEndpoint({

website/src/components/Playground/editor-types/@data-client/rest.d.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,6 +1205,7 @@ type ExtractCollection<S extends Schema | undefined> = S extends ({
12051205
push: any;
12061206
unshift: any;
12071207
assign: any;
1208+
remove: any;
12081209
schema: Schema;
12091210
}) ? S : S extends Object$1<infer T> ? ExtractObject<T> : S extends Exclude<Schema, {
12101211
[K: string]: any;
@@ -1356,6 +1357,12 @@ interface RestInstance<F extends FetchFunction = FetchFunction, S extends Schema
13561357
assign: AddEndpoint<F, ExtractCollection<S>, Exclude<O, 'body' | 'method'> & {
13571358
body: Record<string, OptionsToAdderBodyArgument<O>> | FormData;
13581359
}>;
1360+
/** Remove item(s) (PATCH) from collection
1361+
* @see https://dataclient.io/rest/api/RestEndpoint#remove
1362+
*/
1363+
remove: RemoveEndpoint<F, ExtractCollection<S>['remove'], Exclude<O, 'body' | 'method'> & {
1364+
body: OptionsToAdderBodyArgument<O> | OptionsToAdderBodyArgument<O>[] | FormData;
1365+
}>;
13591366
}
13601367
type RestEndpointExtendOptions<O extends PartialRestGenerics, E extends {
13611368
body?: any;
@@ -1430,17 +1437,39 @@ type PaginationEndpoint<E extends FetchFunction & RestGenerics & {
14301437
}, A extends any[]> = RestInstanceBase<ParamFetchNoBody<A[0], ResolveType<E>>, E['schema'], E['sideEffect'], Pick<E, 'path' | 'searchParams' | 'body'> & {
14311438
searchParams: Omit<A[0], keyof PathArgs<E['path']>>;
14321439
}>;
1433-
type PaginationFieldEndpoint<E extends FetchFunction & RestGenerics & {
1434-
sideEffect?: boolean | undefined;
1435-
}, C extends string> = RestInstanceBase<ParamFetchNoBody<{
1440+
/** Merge pagination field C into body, making it required */
1441+
type PaginationIntoBody<Body, C extends string> = Body & {
1442+
[K in C]: string | number | boolean;
1443+
};
1444+
/** Paginated searchParams type */
1445+
type PaginatedSearchParams<E extends {
1446+
searchParams?: any;
1447+
path?: string;
1448+
}, C extends string> = {
14361449
[K in C]: string | number | boolean;
1437-
} & E['searchParams'] & PathArgs<Exclude<E['path'], undefined>>, ResolveType<E>>, E['schema'], E['sideEffect'], Pick<E, 'path' | 'searchParams' | 'body'> & {
1450+
} & E['searchParams'] & PathArgs<Exclude<E['path'], undefined>>;
1451+
/** searchParams version: pagination in searchParams, optional body support */
1452+
type PaginationFieldInSearchParams<E extends FetchFunction & RestGenerics & {
1453+
sideEffect?: boolean | undefined;
1454+
}, C extends string> = RestInstanceBase<ParamFetchNoBody<PaginatedSearchParams<E, C>, ResolveType<E>> | ParamFetchWithBody<PaginatedSearchParams<E, C>, NonNullable<E['body']>, ResolveType<E>>, E['schema'], E['sideEffect'], Pick<E, 'path' | 'searchParams' | 'body'> & {
14381455
searchParams: {
14391456
[K in C]: string | number | boolean;
14401457
} & E['searchParams'];
14411458
}> & {
14421459
paginationField: C;
14431460
};
1461+
/** body version: pagination field is in body (body required) */
1462+
type PaginationFieldInBody<E extends FetchFunction & RestGenerics & {
1463+
sideEffect?: boolean | undefined;
1464+
}, C extends string> = RestInstanceBase<ParamFetchWithBody<E['searchParams'] & PathArgs<Exclude<E['path'], undefined>>, PaginationIntoBody<E['body'], C>, ResolveType<E>>, E['schema'], E['sideEffect'], Pick<E, 'path' | 'searchParams'> & {
1465+
body: PaginationIntoBody<E['body'], C>;
1466+
}> & {
1467+
paginationField: C;
1468+
};
1469+
/** Retrieves the next page of results by pagination field */
1470+
type PaginationFieldEndpoint<E extends FetchFunction & RestGenerics & {
1471+
sideEffect?: boolean | undefined;
1472+
}, C extends string> = undefined extends E['body'] ? PaginationFieldInSearchParams<E, C> : C extends keyof E['body'] ? PaginationFieldInBody<E, C> : PaginationFieldInSearchParams<E, C>;
14441473
type AddEndpoint<F extends FetchFunction = FetchFunction, S extends Schema | undefined = any, O extends {
14451474
path: string;
14461475
body: any;
@@ -1451,6 +1480,16 @@ type AddEndpoint<F extends FetchFunction = FetchFunction, S extends Schema | und
14511480
}> = RestInstanceBase<RestFetch<'searchParams' extends keyof O ? O['searchParams'] extends undefined ? PathArgs<Exclude<O['path'], undefined>> : O['searchParams'] & PathArgs<Exclude<O['path'], undefined>> : PathArgs<Exclude<O['path'], undefined>>, O['body'], ResolveType<F>>, S, true, Omit<O, 'method'> & {
14521481
method: 'POST';
14531482
}>;
1483+
type RemoveEndpoint<F extends FetchFunction = FetchFunction, S extends Schema | undefined = any, O extends {
1484+
path: string;
1485+
body: any;
1486+
searchParams?: any;
1487+
} = {
1488+
path: string;
1489+
body: any;
1490+
}> = RestInstanceBase<RestFetch<'searchParams' extends keyof O ? O['searchParams'] extends undefined ? PathArgs<Exclude<O['path'], undefined>> : O['searchParams'] & PathArgs<Exclude<O['path'], undefined>> : PathArgs<Exclude<O['path'], undefined>>, O['body'], ResolveType<F>>, S, true, Omit<O, 'method'> & {
1491+
method: 'PATCH';
1492+
}>;
14541493
type OptionsToAdderBodyArgument<O extends {
14551494
body?: any;
14561495
}> = 'body' extends keyof O ? O['body'] : any;
@@ -1798,4 +1837,4 @@ declare class NetworkError extends Error {
17981837
constructor(response: Response);
17991838
}
18001839

1801-
export { type AbstractInstanceType, type AddEndpoint, Array$1 as Array, type CheckLoop, Collection, type CustomResource, type DefaultArgs, type Defaults, type Denormalize, type DenormalizeNullable, type DenormalizeNullableObject, type DenormalizeObject, Endpoint, type EndpointExtendOptions, type EndpointExtraOptions, type EndpointInstance, type EndpointInstanceInterface, type EndpointInterface, type EndpointOptions, type EndpointParam, type EndpointToFunction, type EntitiesInterface, type EntitiesPath, Entity, type EntityFields, type EntityInterface, type EntityMap, EntityMixin, type EntityPath, type EntityTable, type ErrorTypes, type ExpiryStatusInterface, ExtendableEndpoint, type ExtendedResource, type FetchFunction, type FetchGet, type FetchMutate, type FromFallBack, type GetEndpoint, type GetEntity, type GetIndex, type HookResource, type HookableEndpointInterface, type IEntityClass, type IEntityInstance, type INormalizeDelegate, type IQueryDelegate, type RestEndpoint$1 as IRestEndpoint, type IndexPath, Invalidate, type KeyofEndpointInstance, type KeyofRestEndpoint, type KeysToArgs, type Mergeable, type MethodToSide, type MutateEndpoint, type NI, NetworkError, type Normalize, type NormalizeNullable, type NormalizeObject, type NormalizedEntity, type NormalizedIndex, type NormalizedNullableObject, type ObjectArgs, type OptionsToFunction, type PaginationEndpoint, type PaginationFieldEndpoint, type ParamFetchNoBody, type ParamFetchWithBody, type ParamToArgs, type PartialRestGenerics, type PathArgs, type PathArgsAndSearch, type PathKeys, type PolymorphicInterface, type Queryable, type ReadEndpoint, type RecordClass, type ResolveType, type Resource, type ResourceEndpointExtensions, type ResourceExtension, type ResourceGenerics, type ResourceInterface, type ResourceOptions, RestEndpoint, type RestEndpointConstructor, type RestEndpointConstructorOptions, type RestEndpointExtendOptions, type RestEndpointOptions, type RestExtendedEndpoint, type RestFetch, type RestGenerics, type RestInstance, type RestInstanceBase, type RestType, type RestTypeNoBody, type RestTypeWithBody, type Schema, type SchemaArgs, type SchemaClass, type SchemaSimple, type Serializable, type ShortenPath, type SnapshotInterface, type UnknownError, type Visit, resource as createResource, getUrlBase, getUrlTokens, hookifyResource, resource, schema_d as schema, validateRequired };
1840+
export { type AbstractInstanceType, type AddEndpoint, Array$1 as Array, type CheckLoop, Collection, type CustomResource, type DefaultArgs, type Defaults, type Denormalize, type DenormalizeNullable, type DenormalizeNullableObject, type DenormalizeObject, Endpoint, type EndpointExtendOptions, type EndpointExtraOptions, type EndpointInstance, type EndpointInstanceInterface, type EndpointInterface, type EndpointOptions, type EndpointParam, type EndpointToFunction, type EntitiesInterface, type EntitiesPath, Entity, type EntityFields, type EntityInterface, type EntityMap, EntityMixin, type EntityPath, type EntityTable, type ErrorTypes, type ExpiryStatusInterface, ExtendableEndpoint, type ExtendedResource, type FetchFunction, type FetchGet, type FetchMutate, type FromFallBack, type GetEndpoint, type GetEntity, type GetIndex, type HookResource, type HookableEndpointInterface, type IEntityClass, type IEntityInstance, type INormalizeDelegate, type IQueryDelegate, type RestEndpoint$1 as IRestEndpoint, type IndexPath, Invalidate, type KeyofEndpointInstance, type KeyofRestEndpoint, type KeysToArgs, type Mergeable, type MethodToSide, type MutateEndpoint, type NI, NetworkError, type Normalize, type NormalizeNullable, type NormalizeObject, type NormalizedEntity, type NormalizedIndex, type NormalizedNullableObject, type ObjectArgs, type OptionsToFunction, type PaginationEndpoint, type PaginationFieldEndpoint, type ParamFetchNoBody, type ParamFetchWithBody, type ParamToArgs, type PartialRestGenerics, type PathArgs, type PathArgsAndSearch, type PathKeys, type PolymorphicInterface, type Queryable, type ReadEndpoint, type RecordClass, type RemoveEndpoint, type ResolveType, type Resource, type ResourceEndpointExtensions, type ResourceExtension, type ResourceGenerics, type ResourceInterface, type ResourceOptions, RestEndpoint, type RestEndpointConstructor, type RestEndpointConstructorOptions, type RestEndpointExtendOptions, type RestEndpointOptions, type RestExtendedEndpoint, type RestFetch, type RestGenerics, type RestInstance, type RestInstanceBase, type RestType, type RestTypeNoBody, type RestTypeWithBody, type Schema, type SchemaArgs, type SchemaClass, type SchemaSimple, type Serializable, type ShortenPath, type SnapshotInterface, type UnknownError, type Visit, resource as createResource, getUrlBase, getUrlTokens, hookifyResource, resource, schema_d as schema, validateRequired };

0 commit comments

Comments
 (0)