Skip to content

Commit f86e01e

Browse files
committed
enhance: Stricter args typing
1 parent 615d87a commit f86e01e

7 files changed

Lines changed: 589 additions & 15 deletions

File tree

packages/endpoint/src/schemas/Lazy.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,32 @@ import type { Schema, SchemaSimple } from '../interface.js';
22
import type {
33
Denormalize,
44
DenormalizeNullable,
5+
Normalize,
56
NormalizeNullable,
67
} from '../normal.js';
8+
import type { EntityFields } from './EntityFields.js';
9+
10+
type IsAny<T> = 0 extends 1 & T ? true : false;
11+
12+
/** Derives strict Args for LazyQuery from the inner schema S.
13+
*
14+
* - Entity: [EntityFields<U>] for queryKey delegation
15+
* - Collection (or schema with typed queryKey + key): inner schema's Args
16+
* - Everything else: [Normalize<S>] — pass the parent's raw normalized value
17+
*/
18+
export type LazySchemaArgs<S extends Schema> =
19+
S extends { createIfValid: any; pk: any; key: string; prototype: infer U } ?
20+
[EntityFields<U>]
21+
: S extends (
22+
{
23+
queryKey(args: infer Args, ...rest: any): any;
24+
key: string;
25+
}
26+
) ?
27+
IsAny<Args> extends true ?
28+
[Normalize<S>]
29+
: Args
30+
: [Normalize<S>];
731

832
/**
933
* Skips eager denormalization of a relationship field.
@@ -70,10 +94,10 @@ export default class Lazy<S extends Schema> implements SchemaSimple {
7094
* queryKey delegates to inner schema's queryKey if available,
7195
* otherwise passes through args[0] (the raw normalized value).
7296
*/
73-
export class LazyQuery<S extends Schema> implements SchemaSimple<
74-
Denormalize<S>,
75-
readonly any[]
76-
> {
97+
export class LazyQuery<
98+
S extends Schema,
99+
Args = LazySchemaArgs<S>,
100+
> implements SchemaSimple<Denormalize<S>> {
77101
declare schema: S;
78102

79103
constructor(schema: S) {
@@ -100,15 +124,15 @@ export class LazyQuery<S extends Schema> implements SchemaSimple<
100124
}
101125

102126
queryKey(
103-
args: readonly any[],
127+
args: Args,
104128
unvisit: (...args: any) => any,
105129
delegate: { getEntity: any; getIndex: any },
106130
): any {
107131
const schema = this.schema as any;
108132
if (typeof schema.queryKey === 'function' && schema.key) {
109133
return schema.queryKey(args, unvisit, delegate);
110134
}
111-
return args[0];
135+
return (args as readonly any[])[0];
112136
}
113137

114138
declare _denormalizeNullable: (

packages/endpoint/tsconfig.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
"compilerOptions": {
44
"outDir": "lib",
55
"rootDir": "src",
6-
"skipLibCheck": false
6+
"skipLibCheck": false,
7+
"paths": {
8+
"__tests__/*": ["../../__tests__/*"]
9+
}
710
},
811
"include": ["src", "typescript-tests"],
912
"references": [{ "path": "../../__tests__" }]
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
import { IDEntity } from '__tests__/new';
3+
4+
import { useQuery } from '../../react/lib';
5+
import { schema, Collection, Lazy } from '../src';
6+
7+
// --- Entity Definitions ---
8+
9+
class Building extends IDEntity {
10+
readonly name: string = '';
11+
readonly floors: number = 1;
12+
}
13+
14+
class Room extends IDEntity {
15+
readonly label: string = '';
16+
}
17+
18+
class Manager extends IDEntity {
19+
readonly name: string = '';
20+
}
21+
22+
class User extends IDEntity {
23+
readonly type = 'user' as const;
24+
}
25+
class Group extends IDEntity {
26+
readonly type = 'group' as const;
27+
}
28+
29+
// =============================================
30+
// Plain array [Entity]
31+
// =============================================
32+
33+
class DeptWithArray extends IDEntity {
34+
readonly buildings: string[] = [];
35+
static schema = {
36+
buildings: new Lazy([Building]),
37+
};
38+
}
39+
40+
function usePlainArray() {
41+
const _buildings: Building[] | undefined = useQuery(
42+
DeptWithArray.schema.buildings.query,
43+
['bldg-1', 'bldg-2'],
44+
);
45+
46+
// @ts-expect-error - no args at all
47+
useQuery(DeptWithArray.schema.buildings.query);
48+
49+
// @ts-expect-error - too many spread args
50+
useQuery(DeptWithArray.schema.buildings.query, ['bldg-1'], 'extra');
51+
}
52+
53+
// =============================================
54+
// Single Entity
55+
// =============================================
56+
57+
class DeptWithEntity extends IDEntity {
58+
readonly mainBuilding: string = '';
59+
static schema = {
60+
mainBuilding: new Lazy(Building),
61+
};
62+
}
63+
64+
function useSingleEntity() {
65+
const _building: Building | undefined = useQuery(
66+
DeptWithEntity.schema.mainBuilding.query,
67+
{ id: 'bldg-1' },
68+
);
69+
70+
// @ts-expect-error - no args
71+
useQuery(DeptWithEntity.schema.mainBuilding.query);
72+
73+
// @ts-expect-error - wrong key (not a valid Building field)
74+
useQuery(DeptWithEntity.schema.mainBuilding.query, { nonexistent: 'bldg-1' });
75+
76+
// prettier-ignore
77+
// @ts-expect-error - too many args
78+
useQuery(DeptWithEntity.schema.mainBuilding.query, { id: 'bldg-1' }, { id: 'bldg-2' });
79+
}
80+
81+
// =============================================
82+
// schema.Array
83+
// =============================================
84+
85+
class DeptWithSchemaArray extends IDEntity {
86+
readonly buildings: string[] = [];
87+
static schema = {
88+
buildings: new Lazy(new schema.Array(Building)),
89+
};
90+
}
91+
92+
function useSchemaArray() {
93+
const _buildings: Building[] | undefined = useQuery(
94+
DeptWithSchemaArray.schema.buildings.query,
95+
['bldg-1', 'bldg-2'],
96+
);
97+
98+
// @ts-expect-error - no args
99+
useQuery(DeptWithSchemaArray.schema.buildings.query);
100+
101+
// @ts-expect-error - too many args
102+
useQuery(DeptWithSchemaArray.schema.buildings.query, ['a'], 'extra');
103+
}
104+
105+
// =============================================
106+
// schema.Values
107+
// =============================================
108+
109+
class DeptWithValues extends IDEntity {
110+
readonly buildingMap: Record<string, string> = {};
111+
static schema = {
112+
buildingMap: new Lazy(new schema.Values(Building)),
113+
};
114+
}
115+
116+
function useSchemaValues() {
117+
const _valuesResult: Record<string, Building | undefined> | undefined =
118+
useQuery(DeptWithValues.schema.buildingMap.query, {
119+
north: 'bldg-1',
120+
south: 'bldg-2',
121+
});
122+
123+
// @ts-expect-error - no args
124+
useQuery(DeptWithValues.schema.buildingMap.query);
125+
126+
// @ts-expect-error - too many args
127+
useQuery(DeptWithValues.schema.buildingMap.query, { a: '1' }, 'extra');
128+
}
129+
130+
// =============================================
131+
// schema.Object
132+
// =============================================
133+
134+
class DeptWithObject extends IDEntity {
135+
readonly info: { primary: string; secondary: string } = {} as any;
136+
static schema = {
137+
info: new Lazy(new schema.Object({ primary: Building, secondary: Room })),
138+
};
139+
}
140+
141+
function useSchemaObject() {
142+
const _objectResult:
143+
| { primary: Building | undefined; secondary: Room | undefined }
144+
| undefined = useQuery(DeptWithObject.schema.info.query, {
145+
primary: 'bldg-1',
146+
secondary: 'rm-1',
147+
});
148+
149+
// @ts-expect-error - no args
150+
useQuery(DeptWithObject.schema.info.query);
151+
152+
// @ts-expect-error - wrong key (not a member of the Object schema)
153+
useQuery(DeptWithObject.schema.info.query, { totally_wrong: 'value' });
154+
155+
// @ts-expect-error - too many args
156+
useQuery(DeptWithObject.schema.info.query, { id: '1' }, { id: '2' });
157+
}
158+
159+
// =============================================
160+
// Collection (default args)
161+
// =============================================
162+
163+
const buildingsCollection = new Collection([Building]);
164+
class DeptWithCollection extends IDEntity {
165+
static schema = {
166+
buildings: new Lazy(buildingsCollection),
167+
};
168+
}
169+
170+
function useCollectionDefaultArgs() {
171+
const _buildings: Building[] | undefined = useQuery(
172+
DeptWithCollection.schema.buildings.query,
173+
{ departmentId: '1' },
174+
);
175+
176+
// DefaultArgs allows [], [Record], [Record, any] — these are valid by design
177+
useQuery(DeptWithCollection.schema.buildings.query);
178+
useQuery(
179+
DeptWithCollection.schema.buildings.query,
180+
{ departmentId: '1' },
181+
'extra',
182+
);
183+
}
184+
185+
// =============================================
186+
// Collection (typed args)
187+
// =============================================
188+
189+
const typedCollection = new Collection([Building], {
190+
argsKey: (urlParams: { departmentId: string }) => ({
191+
departmentId: urlParams.departmentId,
192+
}),
193+
});
194+
class DeptWithTypedCollection extends IDEntity {
195+
static schema = {
196+
buildings: new Lazy(typedCollection),
197+
};
198+
}
199+
200+
function useCollectionTypedArgs() {
201+
const _buildings: Building[] | undefined = useQuery(
202+
DeptWithTypedCollection.schema.buildings.query,
203+
{ departmentId: '1' },
204+
);
205+
206+
// @ts-expect-error - no args
207+
useQuery(DeptWithTypedCollection.schema.buildings.query);
208+
209+
// @ts-expect-error - wrong arg shape (missing required key)
210+
useQuery(DeptWithTypedCollection.schema.buildings.query, { wrongKey: '1' });
211+
212+
// prettier-ignore
213+
// @ts-expect-error - too many args
214+
useQuery(DeptWithTypedCollection.schema.buildings.query, { departmentId: '1' }, 'extra');
215+
}
216+
217+
// =============================================
218+
// schema.All
219+
// =============================================
220+
221+
class DeptWithAll extends IDEntity {
222+
static schema = {
223+
allBuildings: new Lazy(new schema.All(Building)),
224+
};
225+
}
226+
227+
function useSchemaAll() {
228+
const _allBuildings: Building[] | undefined = useQuery(
229+
DeptWithAll.schema.allBuildings.query,
230+
['bldg-1', 'bldg-2'],
231+
);
232+
233+
// @ts-expect-error - no args
234+
useQuery(DeptWithAll.schema.allBuildings.query);
235+
236+
// @ts-expect-error - too many args
237+
useQuery(DeptWithAll.schema.allBuildings.query, 'a', 'b');
238+
}
239+
240+
// =============================================
241+
// Union
242+
// =============================================
243+
244+
const ownerUnion = new schema.Union({ user: User, group: Group }, 'type');
245+
class DeptWithUnion extends IDEntity {
246+
readonly owner: string = '';
247+
static schema = {
248+
owner: new Lazy(ownerUnion),
249+
};
250+
}
251+
252+
function useUnion() {
253+
const _unionResult: User | Group | undefined = useQuery(
254+
DeptWithUnion.schema.owner.query,
255+
{ id: 'usr-1', schema: 'user' },
256+
);
257+
258+
// @ts-expect-error - no args
259+
useQuery(DeptWithUnion.schema.owner.query);
260+
261+
// @ts-expect-error - wrong key (not a UnionResult field)
262+
useQuery(DeptWithUnion.schema.owner.query, { wrong: 'user' });
263+
264+
// @ts-expect-error - too many args
265+
useQuery(DeptWithUnion.schema.owner.query, { type: 'user' }, 'extra');
266+
}
267+
268+
// =============================================
269+
// Plain object literal { key: Entity }
270+
// =============================================
271+
272+
class DeptWithPlainObject extends IDEntity {
273+
readonly refs: { building: string; room: string } = {} as any;
274+
static schema = {
275+
refs: new Lazy({ building: Building, room: Room }),
276+
};
277+
}
278+
279+
function usePlainObject() {
280+
const _plainObjResult:
281+
| { building: Building | undefined; room: Room | undefined }
282+
| undefined = useQuery(DeptWithPlainObject.schema.refs.query, {
283+
building: 'bldg-1',
284+
room: 'rm-1',
285+
});
286+
287+
// @ts-expect-error - no args
288+
useQuery(DeptWithPlainObject.schema.refs.query);
289+
290+
// @ts-expect-error - wrong key (not a member of the plain object schema)
291+
useQuery(DeptWithPlainObject.schema.refs.query, { nonexistent: '1' });
292+
293+
// @ts-expect-error - too many args
294+
useQuery(DeptWithPlainObject.schema.refs.query, { id: '1' }, { id: '2' });
295+
}

0 commit comments

Comments
 (0)