mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
[SDK] Update Readonly Properties on Query Type (#21261)
This commit is contained in:
5
.changeset/slimy-garlics-fetch.md
Normal file
5
.changeset/slimy-garlics-fetch.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@directus/sdk": patch
|
||||
---
|
||||
|
||||
Updated SDK query type and added type documentation
|
||||
@@ -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
313
docs/guides/sdk/types.md
Normal 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`.
|
||||
|
||||
:::
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user