[SDK] Update Readonly Properties on Query Type (#21261)

This commit is contained in:
Brainslug
2024-02-01 14:35:47 +01:00
committed by GitHub
parent 1f2efa7592
commit f34e05bd30
4 changed files with 326 additions and 7 deletions

View File

@@ -0,0 +1,5 @@
---
"@directus/sdk": patch
---
Updated SDK query type and added type documentation

View File

@@ -14,6 +14,7 @@ export const sections = {
items: [
{ display: 'SDK Quickstart', path: '/guides/sdk/getting-started' },
{ display: 'SDK Authentication', path: '/guides/sdk/authentication' },
{ display: 'SDK Types', path: '/guides/sdk/types' },
],
},
{

313
docs/guides/sdk/types.md Normal file
View File

@@ -0,0 +1,313 @@
---
contributors: Tim de Heiden
description: Learn about all of the ways to manage types with the Directus SDK
---
# Directus SDK Types
The Directus SDK provides TypeScript types used for auto-completion and generating output item types, but these can be
complex to work with. This guide will cover some approaches to more easily work with these types.
This guide assumes you're working with the Directus SDK and using TypeScript 5 or later.
## Setting Up a Schema
A schema contains a root schema type, custom fields on core collections, and a defined type for each available
collection and field.
### The root Schema type
The root schema type is the type provided to the SDK client. It should contain **all available collections**, including
junction collections for many-to-many and many-to-any relations. This type is used by the SDK as a lookup table to
determine what relations exist.
If a collection if defined as an array, it is considered a regular collection with multiple items. If not defined as an
array, but instead as a single type, the collection is considered to be a singleton.
```ts
interface MySchema {
// regular collections are array types
collection_a: CollectionA[];
// singleton collections are singular types
collection_c: CollectionC;
}
```
::: tip Improving Results
For the most reliable results, the root schema types should be kept as pure as possible. This means avoiding unions
(`CollectionA | null`), optional types (`optional_collection?: CollectionA[]`), and preferably inline relational types
(types nested directly on the root schema) except when adding custom fields to core collections.
:::
## Custom Fields on Core Collections
Core collections are provided and required in every Directus project, and are prefixed with `directus_`. Directus
projects can add additional fields to these collections, but should also be included in the schema when initializing the
Directus SDK.
To define custom fields on the core collections, add a type containing only your custom fields as a singular type.
```ts
interface MySchema {
// regular collections are array types
collection_a: CollectionA[];
// singleton collections are singular types
collection_c: CollectionC;
// extend the provided DirectusUser type // [!code ++]
directus_users: CustomUser; // [!code ++]
}
// [!code ++]
interface CustomUser { // [!code ++]
custom_field: string; // [!code ++]
} // [!code ++]
```
::: tip 🪲 Core Collections Typing Bug
There is currently an [unsolved bug](https://github.com/directus/directus/issues/19815) when using Directus core
collections. If you need to include types for core collections, you will need to add an item to the root type starting
with `directus_` for any other core collections to be correctly typed.
When [this bug](https://github.com/directus/directus/issues/19815) is resolved, you may need to remove this temporary
fix.
:::
### Collection Field Types
Most Directus field types will map to one of the TypeScript primitive types (`string`, `number`, `boolean`). There are
some exceptions to this where literal types are used to distinguish between primitives in order to add extra
functionality.
```ts
interface CollectionA {
id: number;
status: string;
toggle: boolean;
}
```
There are currently 3 literal types that can be applied. The first 2 are both used to apply the `count(field)`
[array function](/reference/query.html#array-functions) in the `filter`/`field` auto-complete suggestions, these are the
`'json'` and `'csv'` string literal types. The `'datetime'` string literal type which is used to apply all
[datetime functions](/reference/query.html#datetime-functions) in the `filter`/`field` auto-complete suggestions.
```ts
interface CollectionA {
id: number;
status: string;
toggle: boolean;
tags: 'csv'; // [!code ++]
json_field: 'json'; // [!code ++]
date_created: 'datetime'; // [!code ++]
}
```
In the output types these string literals will get resolved to their appropriate types:
- `'csv'` resolves to `string[]`
- `'datetime'` resolves to `string`
- `'json'` resolves to [`JsonValue`](https://github.com/directus/directus/blob/main/sdk/src/types/output.ts#L105)
::: tip Types to Avoid
Some types should be avoided in the Schema as they may not play well with the type logic: `any` or `any[]`, empty type
`{}`, `never` or `void`.
:::
### Adding Relational Fields
For regular relations without junction collections, define a relation using a union of the primary key type and the
related object.
```ts
interface MySchema {
// regular collections are array types
collection_a: CollectionA[];
collection_b: CollectionB[]; // [!code ++]
// singleton collections are singular types
collection_c: CollectionC;
// extend the provided DirectusUser type
directus_users: CustomUser;
}
interface CollectionB { // [!code ++]
id: string; // [!code ++]
} // [!code ++]
```
#### Many to One
```ts
interface CollectionB {
id: string;
m2o: number | CollectionA; // [!code ++]
}
```
#### One to Many
```ts
interface CollectionB {
id: string;
m2o: number | CollectionA;
o2m: number[] | CollectionA[]; // [!code ++]
}
```
### Working with Junction Collections
For relations that rely on a junction collection, define the junction collection on the root schema and refer to this
new type similar to the one to many relation above.
#### Many to Many
```ts
interface MySchema {
// regular collections are array types
collection_a: CollectionA[];
collection_b: CollectionB[];
// singleton collections are singular types
collection_c: CollectionC;
// many-to-many junction collection // [!code ++]
collection_b_a_m2m: CollectionBA_Many[]; // [!code ++]
// extend the provided DirectusUser type
directus_users: CustomUser;
}
// many-to-many junction table // [!code ++]
interface CollectionBA_Many { // [!code ++]
id: number; // [!code ++]
collection_b_id: string | CollectionB; // [!code ++]
collection_a_id: number | CollectionA; // [!code ++]
} // [!code ++]
interface CollectionB {
id: string;
m2o: number | CollectionA;
o2m: number[] | CollectionA[];
m2m: number[] | CollectionBA_Many[]; // [!code ++]
}
```
#### Many to Any
```ts
interface MySchema {
// regular collections are array types
collection_a: CollectionA[];
collection_b: CollectionB[];
// singleton collections are singular types
collection_c: CollectionC;
// many-to-many junction collection
collection_b_a_m2m: CollectionBA_Many[];
// many-to-any junction collection // [!code ++]
collection_b_a_m2a: CollectionBA_Any[]; // [!code ++]
// extend the provided DirectusUser type
directus_users: CustomUser;
}
// many-to-any junction table // [!code ++]
interface CollectionBA_Any { // [!code ++]
id: number; // [!code ++]
collection_b_id: string | CollectionB; // [!code ++]
collection: 'collection_a' | 'collection_c'; // [!code ++]
item: string | CollectionA | CollectionC; // [!code ++]
} // [!code ++]
interface CollectionB {
id: string;
m2o: number | CollectionA;
o2m: number[] | CollectionA[];
m2m: number[] | CollectionBA_Many[];
m2m: number[] | CollectionBA_Any[]; // [!code ++]
}
```
## Working with Generated Output
```ts
async function getCollectionA() {
return await client.request(
readItems('collection_a', {
fields: ['id']
})
)
}
// generated type that can be used in the component
// resolves to { "id": number } in this example
type GeneratedType = Awaited<ReturnType<typeof getCollectionA>>;
```
### Debugging
The SDK provides some utility generic types to help debug issues. The `Identity<>` generic type can be used to try and
resolve generics to their results. This may not always work, and you may need to reduce the type.
```ts
// the output type from the previous example
type GeneratedType = Awaited<ReturnType<typeof getCollectionA>>;
// when hovering over this type it may look unreadable like:
// Merge<MapFlatFields<object & CollectionA, "id", MappedFunctionFields<MySchema, object & CollectionA>>, {}, MapFlatFields<...>, {}>[]
// should resolve to { id: number; }
type ResolvedType = Identity< GeneratedType[0] >;
```
## Working with input Query Types
For the output types to work properly, the `fields` list needs to be static so the types can read the fields that were
selected in the query.
```ts
// this does not work and resolves to string[], losing all information about the fields themselves
const fields = ["id", "status"];
// correctly resolves to readonly ["id", "status"]
const fields = ["id", "status"] as const;
```
Complete example:
```ts
const query: Query<MySchema, CollectionA> = {
limit: 20,
offset: 0,
};
let search = 'test';
if (search) {
query.search = search;
}
// create a second query for literal/readonly type inference
const query2 = {
...query,
fields: [
"id", "status"
],
} satisfies Query<MySchema, CollectionA>;
const results = await directusClient.request(readItems("collection_a", query2));
// or build the query directly inline
const results2 = await directusClient.request(readItems("collection_a", {
...query,
search,
fields: [
"id", "status"
],
}));
```
::: tip Alias Unsupported
At this time, `alias` has not been typed yet for use in other query parameters like `deep`.
:::

View File

@@ -9,13 +9,13 @@ import type { IfAny, UnpackList } from './utils.js';
*/
export interface Query<Schema extends object, Item> {
readonly fields?: IfAny<Schema, (string | Record<string, any>)[], QueryFields<Schema, Item>> | undefined;
readonly filter?: IfAny<Schema, Record<string, any>, QueryFilter<Schema, Item>> | undefined;
readonly search?: string | undefined;
readonly sort?: IfAny<Schema, string | string[], QuerySort<Schema, Item> | QuerySort<Schema, Item>[]> | undefined;
readonly limit?: number | undefined;
readonly offset?: number | undefined;
readonly page?: number | undefined;
readonly deep?: IfAny<Schema, Record<string, any>, QueryDeep<Schema, Item>> | undefined;
filter?: IfAny<Schema, Record<string, any>, QueryFilter<Schema, Item>> | undefined;
search?: string | undefined;
sort?: IfAny<Schema, string | string[], QuerySort<Schema, Item> | QuerySort<Schema, Item>[]> | undefined;
limit?: number | undefined;
offset?: number | undefined;
page?: number | undefined;
deep?: IfAny<Schema, Record<string, any>, QueryDeep<Schema, Item>> | undefined;
readonly alias?: IfAny<Schema, Record<string, string>, QueryAlias<Schema, Item>> | undefined;
}