forked from strapi/strapi
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: contributor guide for type system concepts (strapi#20120)
Co-authored-by: Ben Irvin <[email protected]> Co-authored-by: Hannah Paine <[email protected]>
- Loading branch information
1 parent
616346a
commit 2d8197c
Showing
7 changed files
with
1,578 additions
and
9 deletions.
There are no files selected for viewing
141 changes: 141 additions & 0 deletions
141
docs/docs/guides/05-type-system/02-concepts/01-schema.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
--- | ||
title: Schema | ||
tags: | ||
- typescript | ||
- type system | ||
- type | ||
- concepts | ||
--- | ||
|
||
The schema is the primary data structure leveraged within the Strapi Type System, defining how content is structured and managed in the application. | ||
|
||
It serves several key functions: | ||
|
||
- **Representation**: At its core, a schema outlines and defines the structure of Strapi content. This is useful when dealing with features that need access to low level schema properties (_e.g. attributes, plugin options, etc..._). | ||
|
||
- **Inference**: The schema allows inferring and configuring numerous other types. This includes entities like `ContentType` or `Component`, among others. | ||
|
||
### Scope | ||
|
||
Schema types represent **loaded** schemas in the context of a Strapi server application and should be used accordingly. | ||
|
||
:::caution | ||
Database models and raw schema definitions (_aka schemas before being loaded by the Strapi server_) are **not** the same types and can't be used interchangeably. | ||
::: | ||
|
||
### Sub-Types | ||
|
||
Each box is a type that extends the base Schema interface. | ||
|
||
In between each box is text that represents the discriminant used to differentiate the subtype from others. | ||
|
||
```mermaid | ||
flowchart TB; | ||
Schema -- "<code>modelType: contentType</code>" ---- ContentTypeSchema | ||
Schema -- "<code>modelType: component</code>" ---- ComponentSchema | ||
ContentTypeSchema -- "<code>kind: collectionType</code>" ---- CollectionTypeSchema | ||
ContentTypeSchema -- "<code>kind: singleType</code>" ---- SingleTypeSchema | ||
``` | ||
|
||
### Properties | ||
|
||
Schema types contain useful information that helps other types know how to interact with the Strapi content. | ||
|
||
This is facilitated through multiple properties. | ||
|
||
#### Options | ||
|
||
A set of properties used to configure the schema. It contains information on features activation among other things. | ||
|
||
This can be really useful to make the types adapt to a given schema. | ||
|
||
For instance, the document service uses the `options.draftAndPublish` property to determine whether it should add publication methods to the service type. | ||
|
||
#### Plugin Options | ||
|
||
These options provide the ability to alter or enhance the behaviour of the system based on specific values. | ||
|
||
If a plugin is enabled, it might bring functionality that can affect how types interact with each other. | ||
|
||
For example, it's possible to add or remove certain entity-service filters from the query type based on whether a plugin is enabled. | ||
|
||
#### Attributes | ||
|
||
Strongly typed schema attributes allows the Type System to infer actual entities types based on their properties. | ||
|
||
For instance, a string attribute will resolve to a primitive string in an entity, whereas a repeatable component attribute will resolve to an array of objects. | ||
|
||
### Usage | ||
|
||
import Tabs from '@theme/Tabs' | ||
import TabItem from '@theme/TabItem' | ||
|
||
<Tabs> | ||
<TabItem value="public" label="Public" default> | ||
When designing public APIs (and in most other scenarios), it's advised to use the high-level schema types found in the `Schema` namespace. | ||
|
||
Schema definitions exported from the `Schema` namespace are targeting the dynamic types found in the public schema registries, and will dynamically adapt to the current context while extending the base Schema types. | ||
|
||
:::info | ||
If the public registries are empty (_e.g. types are not generated yet, not in the context of a Strapi application, ..._), schema types will fallback to their low-level definitions. | ||
::: | ||
|
||
```typescript | ||
import type { Schema } from '@strapi/strapi'; | ||
|
||
declare const schema: Schema.Schema; | ||
declare const contentType: Schema.ContentType; | ||
declare const component: Schema.Component; | ||
|
||
declare function processAnySchema(schema: Schema.Schema): void; | ||
|
||
processAnySchema(schema); // ✅ | ||
processAnySchema(contentType); // ✅ | ||
processAnySchema(component); // ✅ | ||
|
||
declare function processContentTypeSchema(schema: Schema.ContentType): void; | ||
|
||
processContentTypeSchema(schema); // ✅ | ||
processContentTypeSchema(contentType); // ✅ | ||
processContentTypeSchema(component); // ❌ Error, a component schema is not assignable to a content-type schema | ||
|
||
declare function processComponentSchema(schema: Schema.Component): void; | ||
|
||
processComponentSchema(schema); // ✅ | ||
processComponentSchema(contentType); // ❌ Error, a content-type schema is not assignable to a component schema | ||
processComponentSchema(component); // ✅ | ||
``` | ||
</TabItem> | ||
<TabItem value="internal" label="Internal"> | ||
Schema definitions exported from the `Struct` namespace defines the low level type representation of Strapi schemas. | ||
|
||
:::caution | ||
Those types can be useful when you want to validate other types against the base ones, but realistically, the public Schema types should almost always be preferred. | ||
::: | ||
```typescript | ||
import type { Struct } from '@strapi/strapi'; | ||
|
||
declare const schema: Struct.Schema; | ||
declare const contentType: Struct.ContentTypeSchema; | ||
declare const component: Struct.ComponentSchema; | ||
|
||
declare function processAnySchema(schema: Struct.Schema): void; | ||
|
||
processAnySchema(schema); // ✅ | ||
processAnySchema(contentType); // ✅ | ||
processAnySchema(component); // ✅ | ||
|
||
declare function processContentTypeSchema(schema: Struct.ContentTypeSchema): void; | ||
|
||
processContentTypeSchema(schema); // ✅ | ||
processContentTypeSchema(contentType); // ✅ | ||
processContentTypeSchema(component); // ❌ Error, a component schema is not assignable to a content-type schema | ||
|
||
declare function processComponentSchema(schema: Struct.ComponentSchema): void; | ||
|
||
processComponentSchema(schema); // ✅ | ||
processComponentSchema(contentType); // ❌ Error, a content-type schema is not assignable to a component schema | ||
processComponentSchema(component); // ✅ | ||
``` | ||
</TabItem> | ||
</Tabs> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
--- | ||
title: UID | ||
tags: | ||
- typescript | ||
- type system | ||
- type | ||
- concepts | ||
toc_max_heading_level: 4 | ||
--- | ||
|
||
:::note | ||
On this page, **a resource** is considered as **anything that can be identified by a UID**. | ||
|
||
This includes (but is not limited to) controllers, schema, services, policies, middlewares, etc... | ||
::: | ||
|
||
|
||
In the Type System, UIDs play a crucial role in referencing various resources (such as schema and entities) by attaching a unique identifier. | ||
|
||
To put it simply, a UID is a unique (string) literal key used to identify, locate, or access a particular resource within the system. | ||
|
||
:::tip | ||
This makes it the perfect tool to index type registries or to use as a type parameter for resource-centric types. | ||
::: | ||
|
||
### Format | ||
|
||
A UID is composed of 3 different parts: | ||
1. A namespace ([link](#1-namespaces)) | ||
2. A separator ([link](#2-separators)) | ||
3. A name ([link](#3-names)) | ||
|
||
#### 1. Namespaces | ||
|
||
There are two main families of namespaces: | ||
|
||
- Scoped (_aka parametrized_) | ||
- Non-scoped (_aka constants_) | ||
|
||
A third kind exists for component UIDs and is defined only by a dynamic category: `<category>`. | ||
|
||
##### Scoped | ||
|
||
Scoped namespaces are defined by a base name, followed by a separator (`::`) and any string. | ||
|
||
In Strapi there are two of them: | ||
|
||
| Name | Definition | Description | | ||
|--------|:-----------------:|------------------------------------------------------| | ||
| API | `api::<scope>` | Represent a resource present in the `<scope>` API | | ||
| Plugin | `plugin::<scope>` | Represent a resource present in the `<scope>` plugin | | ||
|
||
##### Non-Scoped | ||
|
||
These namespaces are used as a simple prefix and define the origin of a resource. | ||
|
||
Strapi uses three of them to create UIDs | ||
|
||
| Name | Definition | Description | | ||
|--------|:----------:|-------------------------------------------------------------------------------| | ||
| Strapi | `strapi` | Represent a resource present in the core of strapi | | ||
| Admin | `admin` | Represent a resource present in Strapi admin | | ||
| Global | `global` | Rarely used (_e.g. policies or middlewares_), it represents a global resource | | ||
|
||
#### 2. Separators | ||
|
||
There are only two kind of separators: | ||
|
||
- `.` for scoped namespaces (`api::<scope>`, `plugin::<scope>`) and components (`<category>`) | ||
- `::` for others (`admin`, `strapi`, `global`) | ||
|
||
#### 3. Names | ||
|
||
UID names can be any alphanumeric string. | ||
|
||
:::caution | ||
A UID is unique for the kind of resource it's attached to, but **different resource can share the same UID**. | ||
|
||
For instance, it's completely possible to have both a `service` and a `schema` identified by `api::article.article`. | ||
|
||
Since **TypeScript is a structural type system**, it means that **different UIDs resolving to the same literal type can match each other**, thus making it possible to send a service UID to a method expecting a schema UID (if they share the same format). | ||
::: | ||
|
||
### Compatibility Table | ||
|
||
The following table shows, for each kind of UID, what resource they can be associated with. | ||
|
||
:::note | ||
ContentType and Component are referring to both the related schema and entity. | ||
::: | ||
|
||
| | ContentType | Component | Middleware | Policy | Controller | Service | | ||
|--------------------------|:------------------:|:------------------:|:------------------:|:------------------:|:------------------:|:------------------:| | ||
| `api::<scope>.<name>` | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | ||
| `plugin::<scope>.<name>` | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | ||
| `<category>.<name>` | :x: | :white_check_mark: | :x: | :x: | :x: | :x: | | ||
| `strapi::<name>` | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :x: | :x: | | ||
| `admin::<name>` | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | ||
| `global::<name>` | :x: | :x: | :white_check_mark: | :white_check_mark: | :x: | :x: | | ||
|
||
### Usage | ||
|
||
When referencing resource by their UID you'll need to use the `UID` namespace exported from `@strapi/types`. | ||
|
||
```typescript | ||
import type { UID } from '@strapi/types'; | ||
``` | ||
|
||
This namespace contains shortcuts to dynamic UID types built from the public registries so that they always adapt to the current context. | ||
|
||
:::danger | ||
The `UID` namespace is designed to be the main interface used by developers. | ||
|
||
Do not use the `Internal.UID` namespace except if you know what you're doing (low level extends clause, isolated internal code, etc...). | ||
::: | ||
|
||
#### Basic Example | ||
|
||
A common usage is to declare a function that takes a UID as a parameter. | ||
|
||
For our example, let's imagine we want to fetch an entity based on the provided resource UID. | ||
|
||
```typescript | ||
import type { UID, Data } from '@strapi/types'; | ||
|
||
declare function fetch(uid: UID.ContentType): Data.ContentType; | ||
``` | ||
|
||
:::tip | ||
To find an exhaustive list of available UID types, take a look at the [related API reference](http://foo) | ||
::: | ||
|
||
#### Parameter Type Inference | ||
|
||
Now let's say we want to adapt the return type of our function, so that it matches the given UID. | ||
|
||
```typescript | ||
fetch('api::article.article'); | ||
// ^ this should return a Data.Entity<'api::article.article'> | ||
|
||
fetch('admin::user'); | ||
// ^ this should return a Data.Entity<'admin::user'> | ||
``` | ||
To do that, we'll need the function to be able to provide us with the current `uid` type based on usage. | ||
|
||
```typescript | ||
import type { UID, Data } from '@strapi/types'; | ||
|
||
declare function fetch<T extends UID.ContentType>(uid: T): Data.ContentType<T>; | ||
``` | ||
|
||
So what's changed here? | ||
|
||
1. We've forced the `uid` type to be inferred upon usage and stored in a type variable called `T`. | ||
2. We've then re-used `T` to parametrize the `Data.ContentType` type. | ||
|
||
`fetch` will now always return the correct entity depending on which `UID` is sent. | ||
|
||
:::caution | ||
When writing actual code, avoid using `T` as a type variable, and always use meaningful names that will help other developers understand what the variable represents. | ||
|
||
For instance, in our example we could use `TContentTypeUID` instead of just `T`. | ||
::: | ||
|
||
#### Going Further | ||
|
||
It's completely possible to reference `T` in other generic parameters. | ||
|
||
Let's add the possibility to select which fields we want to return for our entity. | ||
|
||
```typescript | ||
import type { UID, Data, Schema } from '@strapi/types'; | ||
|
||
declare function fetch< | ||
T extends UID.ContentType, | ||
F extends Schema.AttributeNames<T> | ||
>(uid: T, fields: F[]): Data.ContentType<T>; | ||
``` | ||
|
||
:::tip | ||
You may have noticed that we're using the inferred UID type (`T`) to reference both: | ||
- An entity (`Data.Entity<T>`) | ||
- A schema (`Schema.AttributeNames<T>`) | ||
|
||
This is because they share the same format and can be used interchangeably. | ||
|
||
For more information, take a look at the [format](#format) and [compatibility table](#compatibility-table) sections. | ||
::: |
Oops, something went wrong.