Skip to content

Commit 5b4877c

Browse files
committed
fix (Input attributes validation): BREAKING: More strict input validation
- Typescript utilities that created hints for keys worked, but not accurately in some places - IsAttribute: Now takes into account "?" undefined and null and combinations. Without this, we could see in the fields() statement, keys that refer to Relation if they have ? undefined or null. - IsNotAttribute: Also now takes into account optionals, undefined and null. This also fixed a problem where in populate() statements we could see the id if it was labeled with optional undefined or null - Utilities now do not translate keys to strings, so IDEA prompts now contain additional messages about the type. - Completely removed ArrayPathImpl, ArrayPath utilities. Not used. - PathImpl, Path. A crucial rewrite. The hints no longer include the main key for relationships. Why? Because there is no point in filtering or sorting something by Category (if it is a Relation), it will cause an obvious error. The list now includes simple attributes and simple attributes of relations. Also “?” undefined and null are taken into account. But in addition there is a solution to get attribute keys for Relation[] which was not there before. There is also a limiter for recursion, because the presence of Relation[] with cyclic recursion will cause a TC error. Added a special utility in which the value of how deep the type will be scanned is set. By standard it is 2 maximum is 5 - FilterRelation now don't accept dot format keys that makes no sense and can be source of potential errors like filtering "category.id" and then again filtering id. It now accepts only parent relation keys and parse query without parsing dot nested keys. - - The test type is complicated by relations, cyclic dependencies, optional, null and undefined types. Added a separate test that checks correctness of typing in necessary scenarios.
1 parent 47a4dda commit 5b4877c

File tree

7 files changed

+388
-151
lines changed

7 files changed

+388
-151
lines changed

src/qq-builder.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { _isDefined, _set } from "./query-utils";
22
import {
33
EntityFilterAttributes,
44
FilterOperatorKey,
5+
FilterRelationKey,
56
GetAttributeType,
67
MultipleAttributeType,
78
OnType,
@@ -385,12 +386,12 @@ export class QQBuilder<
385386
* // $and: [{ nested: { $and: [{ id: { $eq: "value" } }] } }];
386387
* // }
387388
* // }
388-
* @param {FilterOperatorKey} attribute Attribute
389+
* @param {FilterRelationKey} attribute Attribute
389390
* @param {QQBuilderCallback} builderFactory Fabric function that returns builder with filters for relation model
390391
*/
391392
public filterRelation<
392393
RelationModel extends object,
393-
K extends FilterOperatorKey<Model>,
394+
K extends FilterRelationKey<Model>,
394395
RelationConfig extends QueryEngineBuilderConfig
395396
>(
396397
attribute: K,
@@ -411,14 +412,13 @@ export class QQBuilder<
411412
sort: Config["sort"];
412413
filters: [
413414
...Config["filters"],
414-
TransformNestedKey<
415-
K,
416-
ParseFilters<
415+
{
416+
[R in K]: ParseFilters<
417417
RelationConfig["filters"],
418418
RelationConfig["rootLogical"],
419419
RelationConfig["negate"]
420-
>
421-
>
420+
>;
421+
}
422422
];
423423
rootLogical: Config["rootLogical"];
424424
negate: Config["negate"];
@@ -1659,7 +1659,7 @@ export class QQBuilder<
16591659

16601660
return !_isDefined(filterKey)
16611661
? parsedNestedFilters
1662-
: _set({}, filterKey, parsedNestedFilters);
1662+
: { [filterKey]: parsedNestedFilters };
16631663
}
16641664

16651665
const filterType = filter.type;

src/query-types-util.ts

Lines changed: 85 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
declare global {
2+
namespace QueryBuilderConfig {
3+
/**
4+
* Default recursion depth for the "key.subKey" type. Default is 2 levels max is 5 levels
5+
* The larger the number, the greater the load on the Typescript engine.
6+
*/
7+
type DefaultScanDepth = 2;
8+
}
9+
}
10+
111
// <editor-fold desc="Field types">
212
export type StrapiSingleFieldInput<Model extends object> = GetStrictOrWeak<
313
Model,
@@ -15,8 +25,8 @@ export type StrapiSortOptions = "desc" | "asc";
1525

1626
export type SortKey<Model extends object> = GetStrictOrWeak<
1727
Model,
18-
FieldPath<Model>,
19-
FieldPath<Model> | string
28+
FiltersAndSortDotPath<Model>,
29+
FiltersAndSortDotPath<Model> | string
2030
>;
2131

2232
export type StrapiSorts<Model extends object> = Map<
@@ -61,8 +71,14 @@ export type GetAttributeType<Key extends EntityFilterAttributes> = Key extends
6171

6272
export type FilterOperatorKey<Model extends object> = GetStrictOrWeak<
6373
Model,
64-
FieldPath<Model>,
65-
FieldPath<Model> | string
74+
FiltersAndSortDotPath<Model>,
75+
FiltersAndSortDotPath<Model> | string
76+
>;
77+
78+
export type FilterRelationKey<Model extends object> = GetStrictOrWeak<
79+
Model,
80+
GetRelations<Model>,
81+
GetRelations<Model> | string
6682
>;
6783

6884
export type AttributeValues = string | string[] | number | number[] | boolean;
@@ -71,7 +87,7 @@ export interface StrapiAttributesFilter<
7187
Model extends object,
7288
NestedModel extends object = {}
7389
> {
74-
key?: FilterOperatorKey<Model>;
90+
key?: FilterOperatorKey<Model> | FilterRelationKey<Model>;
7591
type?: EntityFilterAttributes;
7692
value?: AttributeValues;
7793
negate?: boolean;
@@ -148,18 +164,6 @@ export interface QueryRawInfo<Model extends object, Data extends object> {
148164
// </editor-fold>
149165

150166
// <editor-fold desc="Input type check utils">
151-
type Primitive =
152-
| null
153-
| undefined
154-
| string
155-
| number
156-
| boolean
157-
| symbol
158-
| bigint
159-
| string[]
160-
| number[]
161-
| boolean[];
162-
163167
type IsTuple<T extends ReadonlyArray<any>> = number extends T["length"]
164168
? false
165169
: true;
@@ -168,56 +172,61 @@ type TupleKey<T extends ReadonlyArray<any>> = Exclude<keyof T, keyof any[]>;
168172

169173
type IsSameType<T1, T2> = T1 extends T2 ? true : false;
170174

171-
type PathImpl<
172-
Key extends string | number,
173-
Value,
174-
BaseType
175-
> = Value extends Primitive
176-
? `${Key}`
177-
: IsSameType<Value, BaseType> extends true // There is trick to prevent typescript crush on cyclic dependencies
178-
? `${Key}` | `${Key}.${keyof Value & string}`
179-
: `${Key}` | `${Key}.${Path<Value>}`;
180-
181-
type Path<Model> = Model extends ReadonlyArray<infer Value>
182-
? IsTuple<Model> extends true
183-
? {
184-
[K in TupleKey<Model>]-?: PathImpl<K & string, Model[K], Model>;
185-
}[TupleKey<Model>]
186-
: { [Key in keyof Model[0]]-?: Key & string }[keyof Model[0]]
187-
: {
188-
[Key in keyof Model]-?: PathImpl<Key & string, Model[Key], Model>;
189-
}[keyof Model];
190-
191-
type FieldPath<TFieldValues extends object> = Path<TFieldValues>;
175+
type Decrement<T extends number> = T extends 5
176+
? 4
177+
: T extends 4
178+
? 3
179+
: T extends 3
180+
? 2
181+
: T extends 2
182+
? 1
183+
: T extends 1
184+
? 0
185+
: never;
192186

193-
type ArrayPathImpl<
187+
type PathImpl<
194188
Key extends string | number,
195189
Value,
196-
BaseType
197-
> = Value extends Primitive
190+
BaseType,
191+
Depth extends number
192+
> = Depth extends 0 // Prevent infinite dependency
193+
? never
194+
: [Value] extends [ModelPrimitive]
195+
? Key
196+
: [Value] extends [ModelPrimitive | undefined]
197+
? Key
198+
: [Value] extends [ModelPrimitive | null]
199+
? Key
200+
: [Value] extends [ModelPrimitive | null | undefined]
201+
? Key
202+
: IsSameType<Value, BaseType> extends true // Prevent cyclic dependency
198203
? never
199-
: Value extends ReadonlyArray<infer U>
200-
? U extends Primitive
201-
? never
202-
: IsSameType<Value, BaseType> extends true // There is trick to prevent typescript crush on cyclic dependencies
203-
? `${Key}CyclicDepsFounded`
204-
: `${Key}` | `${Key}.${ArrayPath<Value>}`
205-
: `${Key}.${ArrayPath<Value>}`;
206-
207-
type ArrayPath<Model> = Model extends ReadonlyArray<infer V>
204+
: `${Key}.${Path<Value, Decrement<Depth>>}`;
205+
206+
type Path<
207+
Model,
208+
Depth extends number = QueryBuilderConfig.DefaultScanDepth extends number
209+
? QueryBuilderConfig.DefaultScanDepth
210+
: 2
211+
> = Model extends ReadonlyArray<infer Value>
208212
? IsTuple<Model> extends true
209213
? {
210-
[Key in TupleKey<Model>]-?: ArrayPathImpl<
214+
[K in TupleKey<Model>]-?: PathImpl<K & string, Model[K], Model, Depth>;
215+
}[TupleKey<Model>]
216+
: {
217+
[Key in keyof Model[0]]-?: PathImpl<
211218
Key & string,
212-
Model[Key],
213-
Model
219+
Model[0][Key],
220+
Model,
221+
Depth
214222
>;
215-
}[TupleKey<Model>]
216-
: { [Key in keyof Model[0]]-?: Key & string }[keyof Model[0]]
223+
}[keyof Model[0]]
217224
: {
218-
[Key in keyof Model]-?: ArrayPathImpl<Key & string, Model[Key], Model>;
225+
[Key in keyof Model]-?: PathImpl<Key & string, Model[Key], Model, Depth>;
219226
}[keyof Model];
220227

228+
type FiltersAndSortDotPath<TFieldValues extends object> = Path<TFieldValues>;
229+
221230
type ModelPrimitive =
222231
| string
223232
| number
@@ -228,15 +237,30 @@ type ModelPrimitive =
228237
| number[]
229238
| boolean[];
230239

231-
type IsAttribute<
232-
Key extends string | number,
233-
Value
234-
> = Value extends ModelPrimitive ? `${Key}` : never;
240+
type IsAttribute<Key extends string | number, Value> = [Value] extends [
241+
ModelPrimitive
242+
]
243+
? Key
244+
: [Value] extends [ModelPrimitive | undefined]
245+
? Key
246+
: [Value] extends [ModelPrimitive | null]
247+
? Key
248+
: [Value] extends [ModelPrimitive | null | undefined]
249+
? Key
250+
: never;
235251

236252
type IsNotAttribute<
237253
Key extends string | number,
238254
Value
239-
> = Value extends ModelPrimitive ? never : `${Key}`;
255+
> = Value extends ModelPrimitive
256+
? never
257+
: Value extends ModelPrimitive | undefined
258+
? never
259+
: Value extends ModelPrimitive | null
260+
? never
261+
: Value extends ModelPrimitive | null | undefined
262+
? never
263+
: Key;
240264

241265
type GetStrictOrWeak<Model extends object, Strict, Weak> = Model extends {
242266
id: infer U;

src/rq-builder.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { _isDefined, _set } from "./query-utils";
22
import {
33
EntityFilterAttributes,
44
FilterOperatorKey,
5+
FilterRelationKey,
56
GetAttributeType,
67
MultipleAttributeType,
78
OnType,
@@ -396,12 +397,12 @@ export class RQBuilder<
396397
* // $and: [{ nested: { $and: [{ id: { $eq: "value" } }] } }];
397398
* // }
398399
* // }
399-
* @param {FilterOperatorKey} attribute Attribute
400+
* @param {FilterRelationKey} attribute Attribute
400401
* @param {RQBuilderCallback} builderFactory Fabric function that returns builder with filters for relation model
401402
*/
402403
public filterRelation<
403404
RelationModel extends object,
404-
K extends FilterOperatorKey<Model>,
405+
K extends FilterRelationKey<Model>,
405406
RelationConfig extends EntityBuilderConfig
406407
>(
407408
attribute: K,
@@ -422,14 +423,13 @@ export class RQBuilder<
422423
sort: Config["sort"];
423424
filters: [
424425
...Config["filters"],
425-
TransformNestedKey<
426-
K,
427-
ParseFilters<
426+
{
427+
[R in K]: ParseFilters<
428428
RelationConfig["filters"],
429429
RelationConfig["rootLogical"],
430430
RelationConfig["negate"]
431-
>
432-
>
431+
>;
432+
}
433433
];
434434
rootLogical: Config["rootLogical"];
435435
negate: Config["negate"];
@@ -1969,7 +1969,7 @@ export class RQBuilder<
19691969

19701970
return !_isDefined(filterKey)
19711971
? parsedNestedFilters
1972-
: _set({}, filterKey, parsedNestedFilters);
1972+
: { [filterKey]: parsedNestedFilters };
19731973
}
19741974

19751975
const filterType = filter.type;

src/sq-builder.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { _isDefined, _set } from "./query-utils";
22
import {
33
EntityFilterAttributes,
44
FilterOperatorKey,
5+
FilterRelationKey,
56
GetAttributeType,
67
MultipleAttributeType,
78
OnType,
@@ -402,12 +403,12 @@ export class SQBuilder<
402403
* // $and: [{ nested: { $and: [{ id: { $eq: "value" } }] } }];
403404
* // }
404405
* // }
405-
* @param {FilterOperatorKey} attribute Attribute
406+
* @param {FilterRelationKey} attribute Attribute
406407
* @param {SQBuilderCallback} builderFactory Fabric function that returns builder with filters for relation model
407408
*/
408409
public filterRelation<
409410
RelationModel extends object,
410-
K extends FilterOperatorKey<Model>,
411+
K extends FilterRelationKey<Model>,
411412
RelationConfig extends EntityBuilderConfig
412413
>(
413414
attribute: K,
@@ -428,14 +429,13 @@ export class SQBuilder<
428429
sort: Config["sort"];
429430
filters: [
430431
...Config["filters"],
431-
TransformNestedKey<
432-
K,
433-
ParseFilters<
432+
{
433+
[R in K]: ParseFilters<
434434
RelationConfig["filters"],
435435
RelationConfig["rootLogical"],
436436
RelationConfig["negate"]
437-
>
438-
>
437+
>;
438+
}
439439
];
440440
rootLogical: Config["rootLogical"];
441441
negate: Config["negate"];
@@ -1866,7 +1866,7 @@ export class SQBuilder<
18661866

18671867
return !_isDefined(filterKey)
18681868
? parsedNestedFilters
1869-
: _set({}, filterKey, parsedNestedFilters);
1869+
: { [filterKey]: parsedNestedFilters };
18701870
}
18711871

18721872
const filterType = filter.type;

tests/entity-service-query-builder/types-tests/fields-typing.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,38 @@
11
import { SQBuilder } from "../../../lib/cjs";
22

3+
export interface DeepModel {
4+
id: string;
5+
deepProp: string;
6+
}
7+
38
export interface NestedModel {
49
id: string;
510
name: string;
11+
deepNested?: DeepModel;
12+
deepNestedList?: DeepModel[];
613
}
714

815
export interface TestModel {
916
id: string;
1017
name: string;
1118
description: string;
1219
options: string;
20+
notNestedEnumeration: string[];
21+
someOptional?: string;
22+
notNestedEnumerationOptional?: string[];
23+
optionalAndNullable?: string | null;
24+
optionalNullableUndefined?: string | null | undefined;
25+
nullableUndefined: string | null | undefined;
1326
nested: NestedModel;
1427
nestedList: NestedModel[];
15-
notNestedEnumeration: string[];
28+
nestedOptional?: NestedModel;
29+
nestedOptionalList?: NestedModel[];
30+
cyclicRelationList: TestModel[];
31+
cyclicRelation: TestModel;
32+
nestedOptionalNullable?: NestedModel | null;
33+
nestedOptionalNullableUndefined?: NestedModel | null | undefined;
34+
nestedNullableUndefined: NestedModel | null | undefined;
35+
nestedListOptionalNullableUndefined?: NestedModel[] | null | undefined;
1636
}
1737

1838
describe("Fields types", () => {

0 commit comments

Comments
 (0)