SDK Support for literal field types (#19792)

Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
This commit is contained in:
Brainslug
2023-09-28 00:02:10 +02:00
committed by GitHub
parent 9f7b1cb973
commit a90235e890
17 changed files with 201 additions and 94 deletions

View File

@@ -0,0 +1,5 @@
---
"@directus/sdk": minor
---
Added support for literal field types in the SDK

View File

@@ -9,7 +9,7 @@ export type DirectusActivity<Schema extends object> = MergeCoreCollection<
id: number;
action: string;
user: DirectusUser<Schema> | string | null;
timestamp: string;
timestamp: 'datetime';
ip: string | null;
user_agent: string | null;
collection: string;

View File

@@ -9,7 +9,7 @@ export type DirectusDashboard<Schema extends object> = MergeCoreCollection<
name: string;
icon: string;
note: string | null;
date_created: string | null;
date_created: 'datetime' | null;
user_created: DirectusUser<Schema> | string | null;
color: string | null;
}

View File

@@ -15,9 +15,9 @@ export type DirectusFile<Schema extends object> = MergeCoreCollection<
type: string | null;
folder: DirectusFolder<Schema> | string | null;
uploaded_by: DirectusUser<Schema> | string | null;
uploaded_on: string;
uploaded_on: 'datetime';
modified_by: DirectusUser<Schema> | string | null;
modified_on: string;
modified_on: 'datetime';
charset: string | null;
filesize: string | null;
width: number | null;

View File

@@ -16,7 +16,7 @@ export type DirectusFlow<Schema extends object> = MergeCoreCollection<
accountability: string | null;
options: Record<string, any> | null;
operation: DirectusOperation<Schema> | string | null;
date_created: string | null;
date_created: 'datetime' | null;
user_created: DirectusUser<Schema> | string | null;
}
>;

View File

@@ -6,7 +6,7 @@ export type DirectusNotification<Schema extends object> = MergeCoreCollection<
'directus_notifications',
{
id: string;
timestamp: string | null;
timestamp: 'datetime' | null;
status: string | null;
recipient: DirectusUser<Schema> | string;
sender: DirectusUser<Schema> | string | null;

View File

@@ -17,7 +17,7 @@ export type DirectusOperation<Schema extends object> = MergeCoreCollection<
resolve: DirectusOperation<Schema> | string | null;
reject: DirectusOperation<Schema> | string | null;
flow: DirectusFlow<Schema> | string;
date_created: string | null;
date_created: 'datetime' | null;
user_created: DirectusUser<Schema> | string | null;
}
>;

View File

@@ -19,7 +19,7 @@ export type DirectusPanel<Schema extends object> = MergeCoreCollection<
width: number;
height: number;
options: Record<string, any> | null;
date_created: string | null;
date_created: 'datetime' | null;
user_created: DirectusUser<Schema> | string | null;
}
>;

View File

@@ -30,7 +30,7 @@ export type DirectusSettings<Schema extends object> = MergeCoreCollection<
storage_default_folder: DirectusFolder<Schema> | string | null;
basemaps: Record<string, any> | null;
mapbox_key: string | null;
module_bar: any | null;
module_bar: 'json' | null;
project_descriptor: string | null;
default_language: string;
custom_aspect_ratios: Record<string, any> | null;

View File

@@ -13,9 +13,9 @@ export type DirectusShare<Schema extends object> = MergeCoreCollection<
role: DirectusRole<Schema> | string | null;
password: string | null;
user_created: DirectusUser<Schema> | string | null;
date_created: string | null;
date_start: string | null;
date_end: string | null;
date_created: 'datetime' | null;
date_start: 'datetime' | null;
date_end: 'datetime' | null;
times_used: number | null;
max_uses: number | null;
}

View File

@@ -1,4 +1,4 @@
import type { MergeCoreCollection } from '../index.js';
import type { DirectusRole, MergeCoreCollection } from '../index.js';
import type { DirectusFile } from './file.js';
/**
@@ -22,9 +22,9 @@ export type DirectusUser<Schema extends object> = MergeCoreCollection<
theme: string | null;
tfa_secret: string | null;
status: string;
role: string | null;
role: DirectusRole<Schema> | string | null;
token: string | null;
last_access: string | null;
last_access: 'datetime' | null;
last_page: string | null;
provider: string;
external_identifier: string | null;

View File

@@ -1,4 +1,5 @@
import type { AllCollections, GetCollection, ItemType, Query, RelationalFields, UnpackList } from './index.js';
import type { ArrayFunctions, DateTimeFunctions, MappedFieldNames, MappedFunctionFields } from './functions.js';
import type { AllCollections, GetCollection, LiteralFields, Query, RelationalFields, UnpackList } from './index.js';
export type GroupingFunctions = {
date: 'year' | 'month' | 'week' | 'day' | 'weekday' | 'hour' | 'minute' | 'second';
@@ -52,8 +53,8 @@ export type AggregateRecord<Fields = string> = {
* GroupBy parameters
*/
export type GroupByFields<Schema extends object, Item> =
| WrappedFields<DateFields<Schema, Item>, GroupingFunctions['date']>
| WrappedFields<RelationalFields<Schema, Item>, GroupingFunctions['array']>;
| WrappedFields<LiteralFields<Item, 'datetime'>, DateTimeFunctions>
| WrappedFields<RelationalFields<Schema, Item>, ArrayFunctions>;
/**
* Aggregation input options
@@ -78,16 +79,18 @@ export type AggregationOutput<
Options extends AggregationOptions<Schema, Collection>
> = ((Options['groupBy'] extends string[]
? UnpackList<GetCollection<Schema, Collection>> extends infer Item
? MappedFunctionFields<Schema, Item> extends infer FieldMap
? MappedFieldNames<Schema, Item> extends infer NamesMap
? {
[Field in UnpackList<Options['groupBy']> as TranslateFunctionField<FieldMap, Field>]: ExtractFieldName<
NamesMap,
Field
> extends keyof Item
? Item[ExtractFieldName<NamesMap, Field>]
: never;
}
? Item extends object
? MappedFunctionFields<Schema, Item> extends infer FieldMap
? MappedFieldNames<Schema, Item> extends infer NamesMap
? {
[Field in UnpackList<Options['groupBy']> as TranslateFunctionField<
FieldMap,
Field
>]: TranslateFunctionField<NamesMap, Field> extends keyof Item
? Item[TranslateFunctionField<NamesMap, Field>]
: never;
}
: never
: never
: never
: never
@@ -111,43 +114,8 @@ type WrappedFields<Fields, Funcs> = Fields extends string
: never;
/**
* Try to detect date fields
* TODO all we can really check is for string types, can we do more?
* Translate function names based on provided map
*/
type DateFields<Schema extends object, Item> = {
[Key in keyof Item]: Extract<Item[Key], ItemType<Schema>> extends never
? NonNullable<Item[Key]> extends string
? Key
: never
: never;
}[keyof Item];
/**
* The types below are helpers for working with fields wrapped in functions
*
* TODO this must be doable in a simpler way to handle the logic below!
*/
type PermuteFields<Fields, Funcs> = Fields extends string ? (Funcs extends string ? [Fields, Funcs] : never) : never;
type MapFunctionFields<Fields, Funcs> = {
[F in PermuteFields<Fields, Funcs> as `${F[1]}(${F[0]})`]: `${F[0]}_${F[1]}`;
};
type MapFieldNames<Fields, Funcs> = {
[F in PermuteFields<Fields, Funcs> as `${F[1]}(${F[0]})`]: F[0];
};
type MappedFunctionFields<Schema extends object, Item> = MapFunctionFields<
DateFields<Schema, Item>,
GroupingFunctions['date']
> &
MapFunctionFields<RelationalFields<Schema, Item>, GroupingFunctions['array']>;
type MappedFieldNames<Schema extends object, Item> = MapFieldNames<
DateFields<Schema, Item>,
GroupingFunctions['date']
> &
MapFieldNames<RelationalFields<Schema, Item>, GroupingFunctions['array']>;
type TranslateFunctionField<FieldMap, Field> = Field extends keyof FieldMap
? FieldMap[Field] extends string
? FieldMap[Field]
@@ -155,11 +123,3 @@ type TranslateFunctionField<FieldMap, Field> = Field extends keyof FieldMap
: Field extends string
? Field
: never;
type ExtractFieldName<FieldMap, Field> = Field extends keyof FieldMap
? FieldMap[Field] extends string
? FieldMap[Field]
: never
: Field extends string
? Field
: never;

View File

@@ -1,3 +1,4 @@
import type { FunctionFields } from './functions.js';
import type { ExtractItem } from './query.js';
import type { ItemType, RelationalFields, RemoveRelationships } from './schema.js';
import type { UnpackList } from './utils.js';
@@ -6,6 +7,7 @@ import type { UnpackList } from './utils.js';
* Fields querying, including nested relational fields
*/
export type QueryFields<Schema extends object, Item> = WrapQueryFields<
Schema,
Item,
QueryFieldsRelational<Schema, UnpackList<Item>>
>;
@@ -13,7 +15,12 @@ export type QueryFields<Schema extends object, Item> = WrapQueryFields<
/**
* Wrap array of fields
*/
export type WrapQueryFields<Item, NestedFields> = readonly ('*' | keyof UnpackList<Item> | NestedFields)[];
export type WrapQueryFields<Schema extends object, Item, NestedFields> = readonly (
| '*'
| keyof UnpackList<Item>
| NestedFields
| FunctionFields<Schema, UnpackList<Item>>
)[];
/**
* Object of nested relational fields in a given Item with it's own fields available for selection
@@ -36,6 +43,7 @@ export type ManyToAnyFields<Schema extends object, Item> = ExtractItem<Schema, I
? 'collection' extends keyof TItem
? 'item' extends keyof TItem
? WrapQueryFields<
Schema,
TItem,
Omit<QueryFieldsRelational<Schema, UnpackList<Item>>, 'item'> & {
item?: {
@@ -76,7 +84,9 @@ export type HasNestedFields<Fields> = UnpackList<Fields> extends infer Field
/**
* Return all keys if Fields is undefined or contains '*'
*/
export type FieldsWildcard<Item extends object, Fields> = UnpackList<Fields> extends infer Field
export type FieldsWildcard<Item extends object, Fields> = unknown extends Fields
? keyof Item
: UnpackList<Fields> extends infer Field
? Field extends undefined
? keyof Item
: Field extends '*'
@@ -107,3 +117,10 @@ type AllKeys<T> = T extends any ? keyof T : never;
export type PickFlatFields<Schema extends object, Item, Fields> = Extract<Fields, keyof Item> extends never
? never
: Pick<RemoveRelationships<Schema, Item>, Extract<Fields, keyof Item>>;
/**
* Extract a specific literal type from a collection
*/
export type LiteralFields<Item, Type extends string> = {
[Key in keyof Item]: Extract<Item[Key], Type>[] extends never[] ? never : Key;
}[keyof Item];

View File

@@ -1,5 +1,6 @@
import type { MappedFieldNames } from './functions.js';
import type { RelationalFields } from './schema.js';
import type { UnpackList } from './utils.js';
import type { Merge, UnpackList } from './utils.js';
/**
* Filters
@@ -10,13 +11,28 @@ export type QueryFilter<Schema extends object, Item> = WrapLogicalFilters<Nested
* Query filters without logical filters
*/
export type NestedQueryFilter<Schema extends object, Item> = UnpackList<Item> extends infer FlatItem
? {
[Field in keyof FlatItem]?:
| (Field extends RelationalFields<Schema, FlatItem>
? WrapRelationalFilters<NestedQueryFilter<Schema, FlatItem[Field]>>
: never)
| FilterOperators<FlatItem[Field]>;
}
? Merge<
{
[Field in keyof FlatItem]?:
| (Field extends RelationalFields<Schema, FlatItem>
? WrapRelationalFilters<NestedQueryFilter<Schema, FlatItem[Field]>>
: never)
| FilterOperators<FlatItem[Field]>;
},
MappedFieldNames<Schema, Item> extends infer Funcs
? {
[Func in keyof Funcs]?: Funcs[Func] extends infer Field
? Field extends keyof FlatItem
?
| (Field extends RelationalFields<Schema, FlatItem>
? WrapRelationalFilters<NestedQueryFilter<Schema, FlatItem[Field]>>
: never)
| FilterOperators<FlatItem[Field]>
: never
: never;
}
: never
>
: never;
/**
@@ -72,8 +88,9 @@ export type WrapRelationalFilters<Filters> =
*/
export type LogicalFilterOperators = '_or' | '_and';
export type WrapLogicalFilters<Filters> =
| {
[Operator in LogicalFilterOperators]?: WrapLogicalFilters<Filters>[];
}
| Filters;
export type WrapLogicalFilters<Filters extends object> = Merge<
{
[Operator in LogicalFilterOperators]?: WrapLogicalFilters<Filters>[];
},
Filters
>;

View File

@@ -1,9 +1,17 @@
import type { LiteralFields } from './fields.js';
import type { ItemType, RelationalFields } from './schema.js';
import type { Merge } from './utils.js';
/**
* Available query functions
*/
export type DateTimeFunctions = 'year' | 'month' | 'week' | 'day' | 'weekday' | 'hour' | 'minute' | 'second';
export type ArrayFunctions = 'count';
export type QueryFunctions = {
date: 'year' | 'month' | 'week' | 'day' | 'weekday' | 'hour' | 'minute' | 'second';
array: 'count';
datetime: DateTimeFunctions;
json: ArrayFunctions;
csv: ArrayFunctions;
};
/**
@@ -14,3 +22,59 @@ export type PermuteFields<Fields, Funcs> = Fields extends string
? [Fields, Funcs]
: never
: never;
/**
* Get all many relations on an item
*/
type RelationalFunctions<Schema extends object, Item> = keyof {
[Key in RelationalFields<Schema, Item> as Extract<Item[Key], ItemType<Schema>> extends any[] ? Key : never]: Key;
};
/**
* Create a map of function fields and their resulting output names
*/
type TranslateFunctionFields<Fields, Funcs> = {
[F in PermuteFields<Fields, Funcs> as `${F[1]}(${F[0]})`]: `${F[0]}_${F[1]}`;
};
/**
* Combine the various function types
*/
export type FunctionFields<Schema extends object, Item> =
| {
[Type in keyof QueryFunctions]: TypeFunctionFields<Item, Type>;
}[keyof QueryFunctions]
| keyof TranslateFunctionFields<RelationalFunctions<Schema, Item>, ArrayFunctions>;
/**
*
*/
export type TypeFunctionFields<Item, Type extends keyof QueryFunctions> = keyof TranslateFunctionFields<
LiteralFields<Item, Type>,
QueryFunctions[Type]
>;
/**
* Map all possible function fields on an item
*/
export type MappedFunctionFields<Schema extends object, Item> = Merge<
TranslateFunctionFields<RelationalFunctions<Schema, Item>, ArrayFunctions>,
TranslateFunctionFields<LiteralFields<Item, 'datetime'>, DateTimeFunctions> &
TranslateFunctionFields<LiteralFields<Item, 'json' | 'csv'>, ArrayFunctions>
>;
/**
* Create a map of function fields with its original field name
*/
type FunctionFieldNames<Fields, Funcs> = {
[F in PermuteFields<Fields, Funcs> as `${F[1]}(${F[0]})`]: F[0];
};
/**
* Map all possible function fields to name on an item
*/
export type MappedFieldNames<Schema extends object, Item> = Merge<
FunctionFieldNames<RelationalFunctions<Schema, Item>, ArrayFunctions>,
FunctionFieldNames<LiteralFields<Item, 'datetime'>, DateTimeFunctions> &
FunctionFieldNames<LiteralFields<Item, 'json' | 'csv'>, ArrayFunctions>
>;

View File

@@ -1,4 +1,5 @@
import type { FieldsWildcard, HasManyToAnyRelation, PickFlatFields, PickRelationalFields } from './fields.js';
import type { FieldsWildcard, HasManyToAnyRelation, PickRelationalFields } from './fields.js';
import type { MappedFunctionFields } from './functions.js';
import type { ItemType } from './schema.js';
import type { IfAny, IsNullable, Merge, Mutable, UnpackList } from './utils.js';
@@ -14,13 +15,15 @@ export type ApplyQueryFields<
CollectionItem extends object = UnpackList<Collection>,
Fields = UnpackList<Mutable<ReadonlyFields>>,
RelationalFields = PickRelationalFields<Fields>,
RelationalKeys = RelationalFields extends never ? never : keyof RelationalFields,
FlatFields = FieldsWildcard<CollectionItem, Exclude<Fields, RelationalKeys>>
RelationalKeys extends keyof RelationalFields = RelationalFields extends never ? never : keyof RelationalFields,
FlatFields extends keyof CollectionItem = FieldsWildcard<CollectionItem, Exclude<Fields, RelationalKeys>>
> = IfAny<
Schema,
Record<string, any>,
Merge<
PickFlatFields<Schema, CollectionItem, FlatFields>,
MappedFunctionFields<Schema, CollectionItem> extends infer FF
? MapFlatFields<CollectionItem, FlatFields, FF extends Record<string, string> ? FF : Record<string, string>>
: never,
RelationalFields extends never
? never
: {
@@ -77,3 +80,38 @@ export type ApplyNestedQueryFields<Schema extends object, Collection, Fields> =
* Carry nullability of
*/
export type RelationNullable<Relation, Output> = IsNullable<Relation, Output | null, Output>;
/**
* Map literal types to actual output types
*/
export type MapFlatFields<
Item extends object,
Fields extends keyof Item,
FunctionMap extends Record<string, string>
> = {
[F in Fields as F extends keyof FunctionMap ? FunctionMap[F] : F]: F extends keyof FunctionMap
? FunctionOutputType
: Extract<Item[F], keyof FieldOutputMap> extends infer A
? A[] extends never[]
? Item[F]
: A extends keyof FieldOutputMap
? FieldOutputMap[A] | Exclude<Item[F], A>
: Item[F]
: Item[F];
};
// Possible JSON types
type JsonPrimitive = null | boolean | number | string;
type JsonValue = JsonPrimitive | JsonPrimitive[] | { [key: string]: JsonValue };
/**
* Output map for specific literal types
*/
export type FieldOutputMap = {
json: JsonValue;
csv: string[];
datetime: string;
};
// all functions return a numeric type
type FunctionOutputType = number;

View File

@@ -24,7 +24,8 @@ export type Merge<A, B, TypeA = NeverToUnknown<A>, TypeB = NeverToUnknown<B>> =
/**
* Fallback never to unknown
*/
export type NeverToUnknown<T> = [T] extends [never] ? unknown : T;
export type NeverToUnknown<T> = IfNever<T, unknown>;
export type IfNever<T, Y> = [T] extends [never] ? Y : T;
/**
* Test for any
@@ -37,3 +38,8 @@ export type IsNullable<T, Y = true, N = never> = T | null extends T ? Y : N;
export type NestedPartial<Item extends object> = {
[Key in keyof Item]?: Item[Key] extends object ? NestedPartial<Item[Key]> : Item[Key];
};
/**
* Resolve type to its final object
*/
export type Identity<U> = U extends infer A ? A : U;