Skip to content

Commit

Permalink
docs: contributor guide for type system concepts (strapi#20120)
Browse files Browse the repository at this point in the history
Co-authored-by: Ben Irvin <[email protected]>
Co-authored-by: Hannah Paine <[email protected]>
  • Loading branch information
3 people authored Apr 19, 2024
1 parent 616346a commit 2d8197c
Show file tree
Hide file tree
Showing 7 changed files with 1,578 additions and 9 deletions.
141 changes: 141 additions & 0 deletions docs/docs/guides/05-type-system/02-concepts/01-schema.mdx
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>
188 changes: 188 additions & 0 deletions docs/docs/guides/05-type-system/02-concepts/02-uid.mdx
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.
:::
Loading

0 comments on commit 2d8197c

Please sign in to comment.