feat: custom type interfaces (#2709)

* feat: ability to hoist type interfaces and reuse them

* docs: organizes ts and gql docs, adds section for field interfaces on both
This commit is contained in:
Jarrod Flesch
2023-05-25 16:32:16 -04:00
committed by Dan Ribbens
parent a518480292
commit 8458a98eff
20 changed files with 2925 additions and 700 deletions

View File

@@ -6,8 +6,10 @@ desc: Array fields are intended for sets of repeating fields, that you define. L
keywords: array, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
---
<Banner >
The Array field type is used when you need to have a set of "repeating" fields. It stores an array of objects containing the fields that you define. Its uses can be simple or highly complex.
<Banner>
The Array field type is used when you need to have a set of "repeating"
fields. It stores an array of objects containing the fields that you define.
Its uses can be simple or highly complex.
</Banner>
**Example uses:**
@@ -17,82 +19,85 @@ keywords: array, fields, config, configuration, documentation, Content Managemen
- Event agenda "timeslots" where you need to specify start & end time ([date field](/docs/fields/date)), label ([text field](/docs/fields/text)), and Learn More page [relationship](/docs/fields/relationship)
![Array field in Payload admin panel](https://payloadcms.com/images/docs/fields/array.jpg)
*Admin panel screenshot of an Array field with a Row containing two text fields, a read-only text field and a checkbox*
_Admin panel screenshot of an Array field with a Row containing two text fields, a read-only text field and a checkbox_
### Config
| Option | Description |
| ---------------- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** * | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** | Text used as the heading in the Admin panel or an object with keys for each language. Auto-generated from name if not defined. |
| **`fields`** * | Array of field types to correspond to each row of the Array. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide an array of row data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Array will be kept, so there is no need to specify each nested field as `localized`. |
| **`required`** | Require this field to have a value. |
| **`labels`** | Customize the row labels appearing in the Admin dashboard. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| Option | Description |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** | Text used as the heading in the Admin panel or an object with keys for each language. Auto-generated from name if not defined. |
| **`fields`** \* | Array of field types to correspond to each row of the Array. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide an array of row data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Array will be kept, so there is no need to specify each nested field as `localized`. |
| **`required`** | Require this field to have a value. |
| **`labels`** | Customize the row labels appearing in the Admin dashboard. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
*\* An asterisk denotes that a property is required.*
_\* An asterisk denotes that a property is required._
### Admin Config
In addition to the default [field admin config](/docs/fields/overview#admin-config), you can adjust the following properties:
| Option | Description |
| ---------------------- | ------------------------------- |
| **`initCollapsed`** | Set the initial collapsed state |
| **`components.RowLabel`** | Function or React component to be rendered as the label on the array row. Receives `({ data, index, path })` as args |
| Option | Description |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| **`initCollapsed`** | Set the initial collapsed state |
| **`components.RowLabel`** | Function or React component to be rendered as the label on the array row. Receives `({ data, index, path })` as args |
### Example
`collections/ExampleCollection.ts`
```ts
import { CollectionConfig } from 'payload/types';
import { CollectionConfig } from "payload/types";
export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
slug: "example-collection",
fields: [
{
name: 'slider', // required
type: 'array', // required
label: 'Image Slider',
name: "slider", // required
type: "array", // required
label: "Image Slider",
minRows: 2,
maxRows: 10,
interfaceName: "CardSlider", // optional
labels: {
singular: 'Slide',
plural: 'Slides',
singular: "Slide",
plural: "Slides",
},
fields: [ // required
fields: [
// required
{
name: 'title',
type: 'text',
name: "title",
type: "text",
},
{
name: 'image',
type: 'upload',
relationTo: 'media',
name: "image",
type: "upload",
relationTo: "media",
required: true,
},
{
name: 'caption',
type: 'text',
}
name: "caption",
type: "text",
},
],
admin: {
components: {
RowLabel: ({ data, index }) => {
return data?.title || `Slide ${String(index).padStart(2, '0')}`;
return data?.title || `Slide ${String(index).padStart(2, "0")}`;
},
},
},
}
]
},
],
};
```

View File

@@ -6,8 +6,11 @@ desc: The Blocks field type is a great layout build and can be used to construct
keywords: blocks, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
---
<Banner >
The Blocks field type is <strong>incredibly powerful</strong> and can be used as a <em>layout builder</em> as well as to define any other flexible content model you can think of. It stores an array of objects, where each object must match the shape of one of your provided block configs.
<Banner>
The Blocks field type is <strong>incredibly powerful</strong> and can be used
as a <em>layout builder</em> as well as to define any other flexible content
model you can think of. It stores an array of objects, where each object must
match the shape of one of your provided block configs.
</Banner>
**Example uses:**
@@ -17,55 +20,61 @@ keywords: blocks, fields, config, configuration, documentation, Content Manageme
- Virtual event agenda "timeslots" where a timeslot could either be a `Break`, a `Presentation`, or a `BreakoutSession`.
![Blocks field in Payload admin panel](https://payloadcms.com/images/docs/fields/blocks.jpg)
*Admin panel screenshot of a Blocks field type with Call to Action and Number block examples*
_Admin panel screenshot of a Blocks field type with Call to Action and Number block examples_
### Field config
| Option | Description |
|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** * | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** | Text used as the heading in the Admin panel or an object with keys for each language. Auto-generated from name if not defined. |
| **`blocks`** * | Array of [block configs](/docs/fields/blocks#block-configs) to be made available to this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
| **`hooks`** | Provide field-level hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-level access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API response or the Admin panel. |
| **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. || **`required`** | Require this field to have a value. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`labels`** | Customize the block row labels appearing in the Admin dashboard. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| Option | Description |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | -------------- | ----------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** | Text used as the heading in the Admin panel or an object with keys for each language. Auto-generated from name if not defined. |
| **`blocks`** \* | Array of [block configs](/docs/fields/blocks#block-configs) to be made available to this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
| **`hooks`** | Provide field-level hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-level access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API response or the Admin panel. |
| **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. | | **`required`** | Require this field to have a value. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`labels`** | Customize the block row labels appearing in the Admin dashboard. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
*\* An asterisk denotes that a property is required.*
_\* An asterisk denotes that a property is required._
### Admin Config
In addition to the default [field admin config](/docs/fields/overview#admin-config), you can adjust the following properties:
| Option | Description |
| ---------------------- | ------------------------------- |
| **`initCollapsed`** | Set the initial collapsed state |
| Option | Description |
| ------------------- | ------------------------------- |
| **`initCollapsed`** | Set the initial collapsed state |
### Block configs
Blocks are defined as separate configs of their own.
<Banner type="success">
<strong>Tip:</strong><br />
Best practice is to define each block config in its own file, and then import them into your Blocks field as necessary. This way each block config can be easily shared between fields. For instance, using the "layout builder" example, you might want to feature a few of the same blocks in a Post collection as well as a Page collection. Abstracting into their own files trivializes their reusability.
<strong>Tip:</strong>
<br />
Best practice is to define each block config in its own file, and then import
them into your Blocks field as necessary. This way each block config can be
easily shared between fields. For instance, using the "layout builder"
example, you might want to feature a few of the same blocks in a Post
collection as well as a Page collection. Abstracting into their own files
trivializes their reusability.
</Banner>
| Option | Description |
|----------------------------|---------------------------------------------------------------------------------------------------------|
| **`slug`** * | Identifier for this block type. Will be saved on each block as the `blockType` property. |
| **`fields`** * | Array of fields to be stored in this block. |
| **`labels`** | Customize the block labels that appear in the Admin dashboard. Auto-generated from slug if not defined. |
| **`imageURL`** | Provide a custom image thumbnail to help editors identify this block in the Admin UI. |
| **`imageAltText`** | Customize this block's image thumbnail alt text. |
| **`graphQL.singularName`** | Text to use for the GraphQL schema name. Auto-generated from slug if not defined |
| Option | Description |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`slug`** \* | Identifier for this block type. Will be saved on each block as the `blockType` property. |
| **`fields`** \* | Array of fields to be stored in this block. |
| **`labels`** | Customize the block labels that appear in the Admin dashboard. Auto-generated from slug if not defined. |
| **`imageURL`** | Provide a custom image thumbnail to help editors identify this block in the Admin UI. |
| **`imageAltText`** | Customize this block's image thumbnail alt text. |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`graphQL.singularName`** | Text to use for the GraphQL schema name. Auto-generated from slug if not defined. NOTE: this is set for deprecation, prefer `interfaceName`. |
#### Auto-generated data per block
@@ -82,41 +91,44 @@ The Admin panel provides each block with a `blockName` field which optionally al
### Example
`collections/ExampleCollection.js`
```ts
import { Block, CollectionConfig } from 'payload/types';
import { Block, CollectionConfig } from "payload/types";
const QuoteBlock: Block = {
slug: 'Quote', // required
imageURL: 'https://google.com/path/to/image.jpg',
imageAltText: 'A nice thumbnail image to show what this block looks like',
fields: [ // required
slug: "Quote", // required
imageURL: "https://google.com/path/to/image.jpg",
imageAltText: "A nice thumbnail image to show what this block looks like",
interfaceName: "QuoteBlock", // optional
fields: [
// required
{
name: 'quoteHeader',
type: 'text',
name: "quoteHeader",
type: "text",
required: true,
},
{
name: 'quoteText',
type: 'text',
name: "quoteText",
type: "text",
},
]
],
};
export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
slug: "example-collection",
fields: [
{
name: 'layout', // required
type: 'blocks', // required
name: "layout", // required
type: "blocks", // required
minRows: 1,
maxRows: 20,
blocks: [ // required
QuoteBlock
]
}
]
}
blocks: [
// required
QuoteBlock,
],
},
],
};
```
### TypeScript
@@ -124,6 +136,5 @@ export const ExampleCollection: CollectionConfig = {
As you build your own Block configs, you might want to store them in separate files but retain typing accordingly. To do so, you can import and use Payload's `Block` type:
```ts
import type { Block } from 'payload/types';
import type { Block } from "payload/types";
```

View File

@@ -6,28 +6,30 @@ desc: The Group field allows other fields to be nested under a common property.
keywords: group, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
---
<Banner >
The Group field allows fields to be nested under a common property name. It also groups fields together visually in the Admin panel.
<Banner>
The Group field allows fields to be nested under a common property name. It
also groups fields together visually in the Admin panel.
</Banner>
### Config
| Option | Description |
| ---------------- | ----------- |
| **`name`** * | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`fields`** * | Array of field types to nest within this Group. |
| **`label`** | Used as a heading in the Admin panel and to name the generated GraphQL type. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide an object of data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Group will be kept, so there is no need to specify each nested field as `localized`. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| Option | Description |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`fields`** \* | Array of field types to nest within this Group. |
| **`label`** | Used as a heading in the Admin panel and to name the generated GraphQL type. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide an object of data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Group will be kept, so there is no need to specify each nested field as `localized`. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
*\* An asterisk denotes that a property is required.*
_\* An asterisk denotes that a property is required._
### Admin config
@@ -40,32 +42,35 @@ Set this property to `true` to hide this field's gutter within the admin panel.
### Example
`collections/ExampleCollection.ts`
```ts
import { CollectionConfig } from 'payload/types';
import { CollectionConfig } from "payload/types";
export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
slug: "example-collection",
fields: [
{
name: 'pageMeta', // required
type: 'group', // required
fields: [ // required
name: "pageMeta", // required
type: "group", // required
interfaceName: "Meta", // optional
fields: [
// required
{
name: 'title',
type: 'text',
name: "title",
type: "text",
required: true,
minLength: 20,
maxLength: 100,
},
{
name: 'description',
type: 'textarea',
name: "description",
type: "textarea",
required: true,
minLength: 40,
maxLength: 160,
}
},
],
}
]
},
],
};
```

View File

@@ -6,71 +6,78 @@ desc: The Tabs field is a great way to organize complex editing experiences into
keywords: tabs, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
---
<Banner >
The Tabs field is presentational-only and only affects the Admin panel (unless a tab is named). By using it, you can place fields within a nice layout component that separates certain sub-fields by a tabbed interface.
<Banner>
The Tabs field is presentational-only and only affects the Admin panel (unless
a tab is named). By using it, you can place fields within a nice layout
component that separates certain sub-fields by a tabbed interface.
</Banner>
![Tabs field type used to separate Hero fields from Page Layout](https://payloadcms.com/images/docs/fields/tabs/tabs.jpg)
*Tabs field type used to separate Hero fields from Page Layout*
_Tabs field type used to separate Hero fields from Page Layout_
### Config
| Option | Description |
| ---------------- | ----------- |
| **`tabs`** * | Array of tabs to render within this Tabs field. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| Option | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **`tabs`** \* | Array of tabs to render within this Tabs field. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
#### Tab-specific Config
Each tab has its own required `label` and `fields` array. You can also optionally pass a `description` to render within each individual tab.
| Option | Description |
| ----------------- | ----------- |
| **`name`** | An optional property name to be used when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** * | The label to render on the tab itself. |
| **`fields`** * | The fields to render within this tab. |
| **`description`** | Optionally render a description within this tab to describe the contents of the tab itself. |
| Option | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`name`** | An optional property name to be used when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** \* | The label to render on the tab itself. |
| **`fields`** \* | The fields to render within this tab. |
| **`description`** | Optionally render a description within this tab to describe the contents of the tab itself. |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). (`name` must be present) |
*\* An asterisk denotes that a property is required.*
_\* An asterisk denotes that a property is required._
### Example
`collections/ExampleCollection.ts`
```ts
import { CollectionConfig } from 'payload/types';
import { CollectionConfig } from "payload/types";
export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
slug: "example-collection",
fields: [
{
type: 'tabs', // required
tabs: [ // required
type: "tabs", // required
tabs: [
// required
{
label: 'Tab One Label', // required
description: 'This will appear within the tab above the fields.',
fields: [ // required
label: "Tab One Label", // required
description: "This will appear within the tab above the fields.",
fields: [
// required
{
name: 'someTextField',
type: 'text',
name: "someTextField",
type: "text",
required: true,
},
],
},
{
name: 'tabTwo',
label: 'Tab Two Label', // required
fields: [ // required
name: "tabTwo",
label: "Tab Two Label", // required
interfaceName: "TabTwo", // optional (`name` must be present)
fields: [
// required
{
name: 'numberField', // accessible via tabTwo.numberField
type: 'number',
name: "numberField", // accessible via tabTwo.numberField
type: "number",
required: true,
},
],
}
]
}
]
}
},
],
},
],
};
```

View File

@@ -8,7 +8,7 @@ keywords: headless cms, typescript, documentation, Content Management System, cm
When working with GraphQL it is useful to have the schema for development of other projects that need to call on your GraphQL endpoint. In Payload the schema is controlled by your collections and globals and is made available to the developer or third parties, it is not necessary for developers using Payload to write schema types manually.
### GraphQL Schema generate script
### Schema generatation script
Run the following command in a Payload project to generate your project's GraphQL schema from Payload:
@@ -16,9 +16,9 @@ Run the following command in a Payload project to generate your project's GraphQ
payload generate:graphQLSchema
```
You can run this command whenever you need to regenerate your graphQL schema and output it to a file, and then you can use the schema for writing your own graphQL elsewhere in other projects.
You can run this command whenever you need to regenerate your GraphQL schema and output it to a file, and then you can use the schema for writing your own GraphQL elsewhere in other projects.
### GraphQL schema output file
### Custom output file path
```js
{
@@ -29,12 +29,51 @@ You can run this command whenever you need to regenerate your graphQL schema and
}
```
### Custom Field Schemas
For `array`, `block`, `group` and named `tab` fields, you can generate top level reusable interfaces. The following group field config:
```ts
{
type: 'group',
name: 'meta',
interfaceName: 'SharedMeta', <-- here!!
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'description',
type: 'text',
},
],
}
```
will generate:
```ts
// a top level reusable type!!
type SharedMeta {
title: String
description: String
}
// example usage inside collection schema
type Collection1 {
// ...other fields
meta: SharedMeta
}
```
The above example outputs all your definitions to a file relative from your payload config as `./graphql/schema.graphql`. By default, the file will be output to your current working directory as `schema.graphql`.
#### Adding an NPM script
<Banner type="warning">
<strong>Important:</strong><br/>
<strong>Important</strong>
<br />
Payload needs to be able to find your config to generate your GraphQL schema.
</Banner>
@@ -45,8 +84,8 @@ To add an NPM script to generate your types and show Payload where to find your
```json
{
"scripts": {
"generate:graphQLSchema": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
},
"generate:graphQLSchema": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema"
}
}
```

View File

@@ -16,13 +16,13 @@ The labels you provide for your Collections and Globals are used to name the Gra
At the top of your Payload config you can define all the options to manage GraphQL.
| Option | Description |
| -------------------- | -------------|
| `mutations` | Any custom Mutations to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
| `queries` | Any custom Queries to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
| `maxComplexity` | A number used to set the maximum allowed complexity allowed by requests [More](/docs/graphql/overview#query-complexity-limits) |
| `disablePlaygroundInProduction` | A boolean that if false will enable the graphQL playground, defaults to true. [More](/docs/graphql/overview#graphql-playground) |
| `disable` | A boolean that if true will disable the graphQL entirely, defaults to false. |
| Option | Description |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mutations` | Any custom Mutations to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
| `queries` | Any custom Queries to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
| `maxComplexity` | A number used to set the maximum allowed complexity allowed by requests [More](/docs/graphql/overview#query-complexity-limits) |
| `disablePlaygroundInProduction` | A boolean that if false will enable the GraphQL playground, defaults to true. [More](/docs/graphql/overview#graphql-playground) |
| `disable` | A boolean that if true will disable the GraphQL entirely, defaults to false. |
| `schemaOutputFile` | A string for the file path used by the generate schema command. Defaults to `graphql.schema` next to `payload.config.ts` [More](/docs/graphql/graphql-schema) |
## Collections
@@ -43,26 +43,26 @@ export const PublicUser: CollectionConfig = {
**Payload will automatically open up the following queries:**
| Query Name | Operation |
| ---------------------- | -------------|
| **`PublicUser`** | `findByID` |
| **`PublicUsers`** | `find` |
| **`mePublicUser`** | `me` auth operation |
| Query Name | Operation |
| ------------------ | ------------------- |
| **`PublicUser`** | `findByID` |
| **`PublicUsers`** | `find` |
| **`mePublicUser`** | `me` auth operation |
**And the following mutations:**
| Query Name | Operation |
| ------------------------------ | -------------|
| **`createPublicUser`** | `create` |
| **`updatePublicUser`** | `update` |
| **`deletePublicUser`** | `delete` |
| Query Name | Operation |
| ------------------------------ | ------------------------------- |
| **`createPublicUser`** | `create` |
| **`updatePublicUser`** | `update` |
| **`deletePublicUser`** | `delete` |
| **`forgotPasswordPublicUser`** | `forgotPassword` auth operation |
| **`resetPasswordPublicUser`** | `resetPassword` auth operation |
| **`unlockPublicUser`** | `unlock` auth operation |
| **`verifyPublicUser`** | `verify` auth operation |
| **`loginPublicUser`** | `login` auth operation |
| **`logoutPublicUser`** | `logout` auth operation |
| **`refreshTokenPublicUser`** | `refresh` auth operation |
| **`resetPasswordPublicUser`** | `resetPassword` auth operation |
| **`unlockPublicUser`** | `unlock` auth operation |
| **`verifyPublicUser`** | `verify` auth operation |
| **`loginPublicUser`** | `login` auth operation |
| **`logoutPublicUser`** | `logout` auth operation |
| **`refreshTokenPublicUser`** | `refresh` auth operation |
## Globals
@@ -81,32 +81,32 @@ const Header: GlobalConfig = {
**Payload will open the following query:**
| Query Name | Operation |
| ---------------------- | -------------|
| **`Header`** | `findOne` |
| Query Name | Operation |
| ------------ | --------- |
| **`Header`** | `findOne` |
**And the following mutation:**
| Query Name | Operation |
| ---------------------- | -------------|
| **`updateHeader`** | `update` |
| Query Name | Operation |
| ------------------ | --------- |
| **`updateHeader`** | `update` |
## Preferences
User [preferences](/docs/admin/overview#preferences) for the admin panel are also available to GraphQL. To query preferences you must supply an authorization token in the header and only the preferences of that user will be accessible and of the `key` argument.
User [preferences](/docs/admin/overview#preferences) for the admin panel are also available to GraphQL. To query preferences you must supply an authorization token in the header and only the preferences of that user will be accessible and of the `key` argument.
**Payload will open the following query:**
| Query Name | Operation |
| ---------------------- | -------------|
| **`Preference`** | `findOne` |
| Query Name | Operation |
| ---------------- | --------- |
| **`Preference`** | `findOne` |
**And the following mutations:**
| Query Name | Operation |
| ---------------------- | -------------|
| **`updatePreference`** | `update` |
| **`deletePreference`** | `delete` |
| Query Name | Operation |
| ---------------------- | --------- |
| **`updatePreference`** | `update` |
| **`deletePreference`** | `delete` |
## GraphQL Playground
@@ -115,8 +115,16 @@ GraphQL Playground is enabled by default for development purposes, but disabled
You can even log in using the `login[collection-singular-label-here]` mutation to use the Playground as an authenticated user.
<Banner type="success">
<strong>Tip:</strong><br/>
To see more regarding how the above queries and mutations are used, visit your GraphQL playground (by default at <a href="http://localhost:3000/api/graphql-playground">(http://localhost:3000/api/graphql-playground</a>) while your server is running. There, you can use the "Schema" and "Docs" buttons on the right to see a ton of detail about how GraphQL operates within Payload.
<strong>Tip:</strong>
<br />
To see more regarding how the above queries and mutations are used, visit your
GraphQL playground (by default at{" "}
<a href="http://localhost:3000/api/graphql-playground">
(http://localhost:3000/api/graphql-playground
</a>
) while your server is running. There, you can use the "Schema" and "Docs"
buttons on the right to see a ton of detail about how GraphQL operates within
Payload.
</Banner>
## Query complexity limits

View File

@@ -8,7 +8,9 @@ keywords: headless cms, typescript, documentation, Content Management System, cm
While building your own custom functionality into Payload, like plugins, hooks, access control functions, custom routes, GraphQL queries / mutations, or anything else, you may benefit from generating your own TypeScript types dynamically from your Payload config itself.
Run the following command in a Payload project to generate types:
### Types generatation script
Run the following command in a Payload project to generate types based on your Payload config:
```
payload generate:types
@@ -16,6 +18,46 @@ payload generate:types
You can run this command whenever you need to regenerate your types, and then you can use these types in your Payload code directly.
### Configuration
In order for Payload to properly infer these types when using local operations, you'll need to alias the following in your tsconfig.json file:
```json
// tsconfig.json
{
"compilerOptions": {
// ...
"paths": {
"payload/generated-types": [
"./src/payload-types.ts" // Ensure this matches the path to your typescript outputFile
]
}
}
// ...
}
```
#### Custom output file path
You can specify where you want your types to be generated by adding a property to your Payload config:
```ts
// payload.config.ts
{
// ...
typescript: {
// defaults to: path.resolve(__dirname, './payload-types.ts')
outputFile: path.resolve(__dirname, './generated-types.ts'),
},
}
```
The above example places your types next to your Payload config itself as the file `generated-types.ts`.
### Example Usage
For example, let's look at the following simple Payload config:
```ts
@@ -74,44 +116,65 @@ export interface Post {
title?: string;
author?: string | User;
}
```
In order for Payload to properly infer these types when using local operations, you'll need to alias the following in your tsconfig.json file:
### Custom Field Interfaces
```json
{
"compilerOptions": {
// ...
"paths": {
"payload/generated-types": [
"./src/payload-types.ts", // Ensure this matches the path to your typescript outputFile
],
}
},
// ...
}
```
#### Customizing the output path of your generated types
You can specify where you want your types to be generated by adding a property to your Payload config:
For `array`, `block`, `group` and named `tab` fields, you can generate top level reusable interfaces. The following group field config:
```ts
{
// the remainder of your config
typescript: {
outputFile: path.resolve(__dirname, './payload-types.ts'),
},
type: 'group',
name: 'meta',
interfaceName: 'SharedMeta', <-- here!!
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'description',
type: 'text',
},
],
}
```
The above example places your types next to your Payload config itself as the file `generated-types.ts`. By default, the file will be output to your current working directory as `payload-types.ts`.
will generate:
```ts
// a top level reusable interface!!
export interface SharedMeta {
title?: string;
description?: string;
}
// example usage inside collection interface
export interface Collection1 {
// ...other fields
meta?: SharedMeta;
}
```
<Banner type="warning">
<strong>Naming Collisions</strong>
<br />
Since these types are hoisted to the top level, you need to be aware that
naming collisions can occur. For example, if you have a collection with the
name of `Meta` and you also create a interface with the name `Meta` they will
collide. It is recommended to scope your interfaces by appending the field
type to the end, i.e. `MetaGroup` or similar.
</Banner>
### Using your types
Now that your types have been generated, payloads local API will now be typed. It is common for users to want to use this in their frontend code, we recommend generating them with payload and then copying the file over to your frontend codebase. This is the simplest way to get your types into your frontend codebase.
#### Adding an NPM script
<Banner type="warning">
<strong>Important:</strong><br/>
<strong>Important</strong>
<br />
Payload needs to be able to find your config to generate your types.
</Banner>

View File

@@ -5,30 +5,27 @@ import { compile } from 'json-schema-to-typescript';
import Logger from '../utilities/logger';
import { SanitizedConfig } from '../config/types';
import loadConfig from '../config/load';
import { entityToJSONSchema, generateEntityObject } from '../utilities/entityToJSONSchema';
import { entityToJSONSchema, generateEntitySchemas } from '../utilities/entityToJSONSchema';
type DefinitionsType = { [k: string]: JSONSchema4 };
function configToJsonSchema(config: SanitizedConfig): JSONSchema4 {
const fieldDefinitionsMap: Map<string, JSONSchema4> = new Map(); // mutable
const entityDefinitions: DefinitionsType = [...config.globals, ...config.collections].reduce((acc, entity) => {
acc[entity.slug] = entityToJSONSchema(config, entity, fieldDefinitionsMap);
return acc;
}, {});
return {
title: 'Config',
type: 'object',
additionalProperties: false,
properties: {
collections: generateEntityObject(config, 'collections'),
globals: generateEntityObject(config, 'globals'),
collections: generateEntitySchemas(config.collections),
globals: generateEntitySchemas(config.globals),
},
required: ['collections', 'globals'],
definitions: Object.fromEntries(
[
...config.globals.map((global) => [
global.slug,
entityToJSONSchema(config, global),
]),
...config.collections.map((collection) => [
collection.slug,
entityToJSONSchema(config, collection),
]),
],
),
definitions: { ...entityDefinitions, ...Object.fromEntries(fieldDefinitionsMap) },
};
}

View File

@@ -218,6 +218,7 @@ export const collapsible = baseField.keys({
const tab = baseField.keys({
name: joi.string().when('localized', { is: joi.exist(), then: joi.required() }),
localized: joi.boolean(),
interfaceName: joi.string().when('name', { not: joi.exist(), then: joi.forbidden() }),
label: joi.alternatives().try(
joi.string(),
joi.object().pattern(joi.string(), [joi.string()]),
@@ -243,6 +244,7 @@ export const group = baseField.keys({
type: joi.string().valid('group').required(),
name: joi.string().required(),
fields: joi.array().items(joi.link('#field')),
interfaceName: joi.string(),
defaultValue: joi.alternatives().try(
joi.object(),
joi.func(),
@@ -277,6 +279,7 @@ export const array = baseField.keys({
RowLabel: componentSchema,
}).default({}),
}).default({}),
interfaceName: joi.string(),
});
export const upload = baseField.keys({
@@ -358,6 +361,7 @@ export const blocks = baseField.keys({
slug: joi.string().required(),
imageURL: joi.string(),
imageAltText: joi.string(),
interfaceName: joi.string(),
graphQL: joi.object().keys({
singularName: joi.string(),
}),

View File

@@ -183,6 +183,13 @@ export type GroupField = Omit<FieldBase, 'required' | 'validation'> & {
admin?: Admin & {
hideGutter?: boolean
}
/** Customize generated GraphQL and Typescript schema names.
* By default it is bound to the collection.
*
* This is useful if you would like to generate a top level type to share amongst collections/fields.
* **Note**: Top level types can collide, ensure they are unique among collections, arrays, groups, blocks, tabs.
*/
interfaceName?: string
}
export type RowAdmin = Omit<Admin, 'description'>;
@@ -207,13 +214,23 @@ export type TabsAdmin = Omit<Admin, 'description'>;
type TabBase = Omit<FieldBase, 'required' | 'validation'> & {
fields: Field[]
description?: Description
interfaceName?: string
}
export type NamedTab = TabBase
export type NamedTab = TabBase & {
/** Customize generated GraphQL and Typescript schema names.
* The slug is used by default.
*
* This is useful if you would like to generate a top level type to share amongst collections/fields.
* **Note**: Top level types can collide, ensure they are unique among collections, arrays, groups, blocks, tabs.
*/
interfaceName?: string
}
export type UnnamedTab = Omit<TabBase, 'name'> & {
label: Record<string, string> | string
localized?: never
interfaceName?: never
}
export type Tab = NamedTab | UnnamedTab
@@ -354,7 +371,7 @@ export type RichTextField = FieldBase & {
}
}
link?: {
fields?: Field[] | ((args: {defaultFields: Field[], config: SanitizedConfig, i18n: Ii18n}) => Field[]);
fields?: Field[] | ((args: { defaultFields: Field[], config: SanitizedConfig, i18n: Ii18n }) => Field[]);
}
}
}
@@ -371,6 +388,13 @@ export type ArrayField = FieldBase & {
RowLabel?: RowLabel
} & Admin['components']
};
/** Customize generated GraphQL and Typescript schema names.
* By default it is bound to the collection.
*
* This is useful if you would like to generate a top level type to share amongst collections/fields.
* **Note**: Top level types can collide, ensure they are unique among collections, arrays, groups, blocks, tabs.
*/
interfaceName?: string
};
export type RadioField = FieldBase & {
@@ -387,9 +411,17 @@ export type Block = {
fields: Field[];
imageURL?: string;
imageAltText?: string;
/** @deprecated - please migrate to the interfaceName property instead. */
graphQL?: {
singularName?: string
}
/** Customize generated GraphQL and Typescript schema names.
* The slug is used by default.
*
* This is useful if you would like to generate a top level type to share amongst collections/fields.
* **Note**: Top level types can collide, ensure they are unique among collections, arrays, groups, blocks, tabs.
*/
interfaceName?: string
}
export type BlockField = FieldBase & {

View File

@@ -15,6 +15,9 @@ export default function registerSchema(payload: Payload): void {
payload.types = {
blockTypes: {},
blockInputTypes: {},
groupTypes: {},
arrayTypes: {},
tabTypes: {},
};
if (payload.config.localization) {

View File

@@ -1,43 +0,0 @@
/* eslint-disable no-param-reassign */
import { Payload } from '../../payload';
import { Block } from '../../fields/config/types';
import buildObjectType from './buildObjectType';
import { toWords } from '../../utilities/formatLabels';
type Args = {
payload: Payload
block: Block
forceNullable?: boolean
}
function buildBlockType({
payload,
block,
forceNullable,
}: Args): void {
const {
slug,
graphQL: {
singularName,
} = {},
} = block;
if (!payload.types.blockTypes[slug]) {
const formattedBlockName = singularName || toWords(slug, true);
payload.types.blockTypes[slug] = buildObjectType({
payload,
name: formattedBlockName,
parentName: formattedBlockName,
fields: [
...block.fields,
{
name: 'blockType',
type: 'text',
},
],
forceNullable,
});
}
}
export default buildBlockType;

View File

@@ -48,7 +48,6 @@ import createRichTextRelationshipPromise from '../../fields/richText/richTextRel
import formatOptions from '../utilities/formatOptions';
import { Payload } from '../../payload';
import buildWhereInputType from './buildWhereInputType';
import buildBlockType from './buildBlockType';
import isFieldNullable from './isFieldNullable';
type LocaleInputType = {
@@ -427,17 +426,20 @@ function buildObjectType({
};
},
array: (objectTypeConfig: ObjectTypeConfig, field: ArrayField) => {
const fullName = combineParentName(parentName, toWords(field.name, true));
const interfaceName = field?.interfaceName || combineParentName(parentName, toWords(field.name, true));
const type = buildObjectType({
payload,
name: fullName,
fields: field.fields,
parentName: fullName,
forceNullable: isFieldNullable(field, forceNullable),
});
if (!payload.types.arrayTypes[interfaceName]) {
// eslint-disable-next-line no-param-reassign
payload.types.arrayTypes[interfaceName] = buildObjectType({
payload,
name: interfaceName,
parentName: interfaceName,
fields: field.fields,
forceNullable: isFieldNullable(field, forceNullable),
});
}
const arrayType = new GraphQLList(new GraphQLNonNull(type));
const arrayType = new GraphQLList(new GraphQLNonNull(payload.types.arrayTypes[interfaceName]));
return {
...objectTypeConfig,
@@ -445,27 +447,44 @@ function buildObjectType({
};
},
group: (objectTypeConfig: ObjectTypeConfig, field: GroupField) => {
const fullName = combineParentName(parentName, toWords(field.name, true));
const type = buildObjectType({
payload,
name: fullName,
parentName: fullName,
fields: field.fields,
forceNullable: isFieldNullable(field, forceNullable),
});
const interfaceName = field?.interfaceName || combineParentName(parentName, toWords(field.name, true));
if (!payload.types.groupTypes[interfaceName]) {
// eslint-disable-next-line no-param-reassign
payload.types.groupTypes[interfaceName] = buildObjectType({
payload,
name: interfaceName,
parentName: interfaceName,
fields: field.fields,
forceNullable: isFieldNullable(field, forceNullable),
});
}
return {
...objectTypeConfig,
[field.name]: { type },
[field.name]: { type: payload.types.groupTypes[interfaceName] },
};
},
blocks: (objectTypeConfig: ObjectTypeConfig, field: BlockField) => {
const blockTypes = field.blocks.map((block) => {
buildBlockType({
payload,
block,
forceNullable: isFieldNullable(field, forceNullable),
});
if (!payload.types.blockTypes[block.slug]) {
const interfaceName = block?.interfaceName || block?.graphQL?.singularName || toWords(block.slug, true);
// eslint-disable-next-line no-param-reassign
payload.types.blockTypes[block.slug] = buildObjectType({
payload,
name: interfaceName,
parentName: interfaceName,
fields: [
...block.fields,
{
name: 'blockType',
type: 'text',
},
],
forceNullable,
});
}
return payload.types.blockTypes[block.slug];
});
@@ -494,18 +513,22 @@ function buildObjectType({
}, objectTypeConfig),
tabs: (objectTypeConfig: ObjectTypeConfig, field: TabsField) => field.tabs.reduce((tabSchema, tab) => {
if (tabHasName(tab)) {
const fullName = combineParentName(parentName, toWords(tab.name, true));
const type = buildObjectType({
payload,
name: fullName,
parentName: fullName,
fields: tab.fields,
forceNullable,
});
const interfaceName = tab?.interfaceName || combineParentName(parentName, toWords(tab.name, true));
if (!payload.types.tabTypes[interfaceName]) {
// eslint-disable-next-line no-param-reassign
payload.types.tabTypes[interfaceName] = buildObjectType({
payload,
name: interfaceName,
parentName: interfaceName,
fields: tab.fields,
forceNullable,
});
}
return {
...tabSchema,
[tab.name]: { type },
[tab.name]: { type: payload.types.tabTypes[interfaceName] },
};
}

View File

@@ -116,6 +116,9 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
types: {
blockTypes: any;
blockInputTypes: any;
groupTypes: any;
arrayTypes: any;
tabTypes: any;
localeInputType?: any;
fallbackLocaleInputType?: any;
};

View File

@@ -43,351 +43,363 @@ function returnOptionEnums(options: Option[]): string[] {
});
}
function generateFieldTypes(config: SanitizedConfig, fields: Field[]): {
function entityFieldsToJSONSchema(config: SanitizedConfig, fields: Field[], fieldDefinitionsMap: Map<string, JSONSchema4>): {
properties: {
[k: string]: JSONSchema4;
}
required: string[]
} {
let topLevelProps = [];
let requiredTopLevelProps = [];
// required fields for a schema (could be for a nested schema)
const requiredFields = new Set<string>(fields.filter(propertyIsRequired).map((field) => (fieldAffectsData(field) ? field.name : '')));
return {
properties: Object.fromEntries(
fields.reduce((properties, field) => {
let fieldSchema: JSONSchema4;
properties: Object.fromEntries(fields.reduce((acc, field) => {
let fieldSchema: JSONSchema4;
switch (field.type) {
case 'text':
case 'textarea':
case 'code':
case 'email':
case 'date': {
fieldSchema = { type: 'string' };
break;
}
switch (field.type) {
case 'text':
case 'textarea':
case 'code':
case 'email':
case 'date': {
fieldSchema = { type: 'string' };
break;
}
case 'number': {
fieldSchema = { type: 'number' };
break;
}
case 'number': {
fieldSchema = { type: 'number' };
break;
}
case 'checkbox': {
fieldSchema = { type: 'boolean' };
break;
}
case 'checkbox': {
fieldSchema = { type: 'boolean' };
break;
}
case 'json': {
// https://www.rfc-editor.org/rfc/rfc7159#section-3
fieldSchema = {
oneOf: [
{ type: 'object' },
{ type: 'array' },
{ type: 'string' },
{ type: 'number' },
{ type: 'boolean' },
{ type: 'null' },
],
};
break;
}
case 'json': {
// https://www.rfc-editor.org/rfc/rfc7159#section-3
fieldSchema = {
oneOf: [
{ type: 'object' },
{ type: 'array' },
{ type: 'string' },
{ type: 'number' },
{ type: 'boolean' },
{ type: 'null' },
],
};
break;
}
case 'richText': {
fieldSchema = {
type: 'array',
items: {
type: 'object',
},
};
case 'richText': {
break;
}
case 'radio': {
fieldSchema = {
type: 'string',
enum: returnOptionEnums(field.options),
};
break;
}
case 'select': {
const selectType: JSONSchema4 = {
type: 'string',
enum: returnOptionEnums(field.options),
};
if (field.hasMany) {
fieldSchema = {
type: 'array',
items: {
type: 'object',
items: selectType,
};
} else {
fieldSchema = selectType;
}
break;
}
case 'point': {
fieldSchema = {
type: 'array',
minItems: 2,
maxItems: 2,
items: [
{
type: 'number',
},
};
break;
}
case 'radio': {
fieldSchema = {
type: 'string',
enum: returnOptionEnums(field.options),
};
break;
}
case 'select': {
const selectType: JSONSchema4 = {
type: 'string',
enum: returnOptionEnums(field.options),
};
{
type: 'number',
},
],
};
break;
}
case 'relationship': {
if (Array.isArray(field.relationTo)) {
if (field.hasMany) {
fieldSchema = {
type: 'array',
items: selectType,
};
} else {
fieldSchema = selectType;
}
oneOf: [
{
type: 'array',
items: {
oneOf: field.relationTo.map((relation) => {
const idFieldType = getCollectionIDType(config.collections, relation);
break;
}
case 'point': {
fieldSchema = {
type: 'array',
minItems: 2,
maxItems: 2,
items: [
{
type: 'number',
},
{
type: 'number',
},
],
};
break;
}
case 'relationship': {
if (Array.isArray(field.relationTo)) {
if (field.hasMany) {
fieldSchema = {
oneOf: [
{
type: 'array',
items: {
oneOf: field.relationTo.map((relation) => {
const idFieldType = getCollectionIDType(config.collections, relation);
return {
type: 'object',
additionalProperties: false,
properties: {
value: {
type: idFieldType,
},
relationTo: {
const: relation,
},
},
required: ['value', 'relationTo'],
};
}),
},
},
{
type: 'array',
items: {
oneOf: field.relationTo.map((relation) => {
return {
type: 'object',
additionalProperties: false,
properties: {
value: {
$ref: `#/definitions/${relation}`,
},
relationTo: {
const: relation,
},
},
required: ['value', 'relationTo'],
};
}),
},
},
],
};
} else {
fieldSchema = {
oneOf: field.relationTo.map((relation) => {
const idFieldType = getCollectionIDType(config.collections, relation);
return {
type: 'object',
additionalProperties: false,
properties: {
value: {
oneOf: [
{
return {
type: 'object',
additionalProperties: false,
properties: {
value: {
type: idFieldType,
},
{
relationTo: {
const: relation,
},
},
required: ['value', 'relationTo'],
};
}),
},
},
{
type: 'array',
items: {
oneOf: field.relationTo.map((relation) => {
return {
type: 'object',
additionalProperties: false,
properties: {
value: {
$ref: `#/definitions/${relation}`,
},
],
},
relationTo: {
const: relation,
},
},
required: ['value', 'relationTo'],
};
}),
};
}
relationTo: {
const: relation,
},
},
required: ['value', 'relationTo'],
};
}),
},
},
],
};
} else {
const idFieldType = getCollectionIDType(config.collections, field.relationTo);
if (field.hasMany) {
fieldSchema = {
oneOf: [
{
type: 'array',
items: {
type: idFieldType,
},
},
{
type: 'array',
items: {
$ref: `#/definitions/${field.relationTo}`,
},
},
],
};
} else {
fieldSchema = {
oneOf: [
{
type: idFieldType,
},
{
$ref: `#/definitions/${field.relationTo}`,
},
],
};
}
}
break;
}
case 'upload': {
const idFieldType = getCollectionIDType(config.collections, field.relationTo);
fieldSchema = {
oneOf: [
{
type: idFieldType,
},
{
$ref: `#/definitions/${field.relationTo}`,
},
],
};
break;
}
case 'blocks': {
fieldSchema = {
type: 'array',
items: {
oneOf: field.blocks.map((block) => {
const blockSchema = generateFieldTypes(config, block.fields);
fieldSchema = {
oneOf: field.relationTo.map((relation) => {
const idFieldType = getCollectionIDType(config.collections, relation);
return {
type: 'object',
additionalProperties: false,
properties: {
...blockSchema.properties,
blockType: {
const: block.slug,
value: {
oneOf: [
{
type: idFieldType,
},
{
$ref: `#/definitions/${relation}`,
},
],
},
relationTo: {
const: relation,
},
},
required: [
'blockType',
...blockSchema.required,
],
required: ['value', 'relationTo'],
};
}),
},
};
break;
}
};
}
} else {
const idFieldType = getCollectionIDType(config.collections, field.relationTo);
case 'array': {
fieldSchema = {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
...generateFieldTypes(config, field.fields),
},
};
break;
}
case 'row':
case 'collapsible': {
const topLevelFields = generateFieldTypes(config, field.fields);
requiredTopLevelProps = requiredTopLevelProps.concat(topLevelFields.required);
topLevelProps = topLevelProps.concat(Object.entries(topLevelFields.properties).map((prop) => prop));
break;
}
case 'tabs': {
field.tabs.forEach((tab) => {
if (tabHasName(tab)) {
requiredTopLevelProps.push(tab.name);
topLevelProps.push([
tab.name,
if (field.hasMany) {
fieldSchema = {
oneOf: [
{
type: 'object',
additionalProperties: false,
...generateFieldTypes(config, tab.fields),
type: 'array',
items: {
type: idFieldType,
},
},
]);
} else {
const topLevelFields = generateFieldTypes(config, tab.fields);
requiredTopLevelProps = requiredTopLevelProps.concat(topLevelFields.required);
topLevelProps = topLevelProps.concat(Object.entries(topLevelFields.properties).map((prop) => prop));
}
});
break;
{
type: 'array',
items: {
$ref: `#/definitions/${field.relationTo}`,
},
},
],
};
} else {
fieldSchema = {
oneOf: [
{
type: idFieldType,
},
{
$ref: `#/definitions/${field.relationTo}`,
},
],
};
}
}
case 'group': {
fieldSchema = {
type: 'object',
additionalProperties: false,
...generateFieldTypes(config, field.fields),
};
break;
}
default: {
break;
}
break;
}
if (fieldSchema && fieldAffectsData(field)) {
return [
...properties,
[
field.name,
case 'upload': {
const idFieldType = getCollectionIDType(config.collections, field.relationTo);
fieldSchema = {
oneOf: [
{
...fieldSchema,
type: idFieldType,
},
{
$ref: `#/definitions/${field.relationTo}`,
},
],
];
};
break;
}
return [
...properties,
...topLevelProps,
];
}, []),
),
required: [
...fields
.filter(propertyIsRequired)
.map((field) => (fieldAffectsData(field) ? field.name : '')),
...requiredTopLevelProps,
],
case 'blocks': {
fieldSchema = {
type: 'array',
items: {
oneOf: field.blocks.map((block) => {
const blockFieldSchemas = entityFieldsToJSONSchema(config, block.fields, fieldDefinitionsMap);
const blockSchema: JSONSchema4 = {
type: 'object',
additionalProperties: false,
properties: {
...blockFieldSchemas.properties,
blockType: {
const: block.slug,
},
},
required: [
'blockType',
...blockFieldSchemas.required,
],
};
if (block.interfaceName) {
fieldDefinitionsMap.set(block.interfaceName, blockSchema);
return {
$ref: `#/definitions/${block.interfaceName}`,
};
}
return blockSchema;
}),
},
};
break;
}
case 'array': {
fieldSchema = {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
...entityFieldsToJSONSchema(config, field.fields, fieldDefinitionsMap),
},
};
if (field.interfaceName) {
fieldDefinitionsMap.set(field.interfaceName, fieldSchema);
fieldSchema = {
$ref: `#/definitions/${field.interfaceName}`,
};
}
break;
}
case 'row':
case 'collapsible': {
const childSchema = entityFieldsToJSONSchema(config, field.fields, fieldDefinitionsMap);
Object.entries(childSchema.properties).forEach(([propName, propSchema]) => {
acc.set(propName, propSchema);
});
childSchema.required.forEach((propName) => {
requiredFields.add(propName);
});
break;
}
case 'tabs': {
field.tabs.forEach((tab) => {
const childSchema = entityFieldsToJSONSchema(config, tab.fields, fieldDefinitionsMap);
if (tabHasName(tab)) {
// could have interface
acc.set(tab.name, {
type: 'object',
additionalProperties: false,
...childSchema,
});
requiredFields.add(tab.name);
} else {
Object.entries(childSchema.properties).forEach(([propName, propSchema]) => {
acc.set(propName, propSchema);
});
childSchema.required.forEach((propName) => {
requiredFields.add(propName);
});
}
});
break;
}
case 'group': {
fieldSchema = {
type: 'object',
additionalProperties: false,
...entityFieldsToJSONSchema(config, field.fields, fieldDefinitionsMap),
};
if (field.interfaceName) {
fieldDefinitionsMap.set(field.interfaceName, fieldSchema);
fieldSchema = {
$ref: `#/definitions/${field.interfaceName}`,
};
}
break;
}
default: {
break;
}
}
if (fieldSchema && fieldAffectsData(field)) {
acc.set(field.name, fieldSchema);
}
return acc;
}, new Map<string, JSONSchema4>())),
required: Array.from(requiredFields),
};
}
export function entityToJSONSchema(config: SanitizedConfig, incomingEntity: SanitizedCollectionConfig | SanitizedGlobalConfig): JSONSchema4 {
export function entityToJSONSchema(config: SanitizedConfig, incomingEntity: SanitizedCollectionConfig | SanitizedGlobalConfig, fieldDefinitionsMap: Map<string, JSONSchema4>): JSONSchema4 {
const entity: SanitizedCollectionConfig | SanitizedGlobalConfig = deepCopyObject(incomingEntity);
const title = entity.typescript?.interface ? entity.typescript.interface : singular(toWords(entity.slug, true));
@@ -424,20 +436,23 @@ export function entityToJSONSchema(config: SanitizedConfig, incomingEntity: Sani
title,
type: 'object',
additionalProperties: false,
...generateFieldTypes(config, entity.fields),
...entityFieldsToJSONSchema(config, entity.fields, fieldDefinitionsMap),
};
}
export function generateEntityObject(config: SanitizedConfig, type: 'collections' | 'globals'): JSONSchema4 {
export function generateEntitySchemas(entities: (SanitizedCollectionConfig | SanitizedGlobalConfig)[]): JSONSchema4 {
const properties = [...entities].reduce((acc, { slug }) => {
acc[slug] = {
$ref: `#/definitions/${slug}`,
};
return acc;
}, {});
return {
type: 'object',
properties: Object.fromEntries(config[type].map(({ slug }) => [
slug,
{
$ref: `#/definitions/${slug}`,
},
])),
required: config[type].map(({ slug }) => slug),
properties,
required: Object.keys(properties),
additionalProperties: false,
};
}

View File

@@ -8,6 +8,7 @@
export interface Config {
collections: {
posts: Post;
media: Media;
users: User;
};
globals: {
@@ -17,18 +18,30 @@ export interface Config {
export interface Post {
id: string;
text?: string;
createdAt: string;
associatedMedia?: string | Media;
updatedAt: string;
createdAt: string;
}
export interface Media {
id: string;
updatedAt: string;
createdAt: string;
url?: string;
filename?: string;
mimeType?: string;
filesize?: number;
width?: number;
height?: number;
}
export interface User {
id: string;
updatedAt: string;
createdAt: string;
email?: string;
resetPasswordToken?: string;
resetPasswordExpiration?: string;
loginAttempts?: number;
lockUntil?: string;
createdAt: string;
updatedAt: string;
password?: string;
}
export interface Menu {

View File

@@ -1,84 +1,145 @@
import path from 'path';
import type { CollectionConfig } from '../../src/collections/config/types';
import { buildConfig } from '../buildConfig';
export interface Relation {
id: string;
name: string;
}
const openAccess = {
create: () => true,
read: () => true,
update: () => true,
delete: () => true,
};
const collectionWithName = (collectionSlug: string): CollectionConfig => {
return {
slug: collectionSlug,
access: openAccess,
fields: [
{
name: 'name',
type: 'text',
},
],
};
};
export const slug = 'posts';
export const relationSlug = 'relation';
export default buildConfig({
graphQL: {
schemaOutputFile: path.resolve(__dirname, 'generated-schema.graphql'),
schemaOutputFile: path.resolve(__dirname, 'schema.graphql'),
},
typescript: {
outputFile: path.resolve(__dirname, 'schema.ts'),
},
collections: [
{
slug,
access: openAccess,
slug: 'collection1',
fields: [
{
name: 'title',
type: 'text',
type: 'row',
fields: [{ type: 'text', required: true, name: 'testing' }],
},
{
name: 'description',
type: 'text',
type: 'tabs',
tabs: [
{
label: 'Tab 1',
fields: [
{
required: true,
type: 'text',
name: 'title',
},
],
},
],
},
{
name: 'number',
type: 'number',
type: 'array',
name: 'meta',
interfaceName: 'SharedMetaArray',
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'description',
type: 'text',
},
],
},
// Relationship
{
name: 'relationField',
type: 'relationship',
relationTo: relationSlug,
},
// Relation hasMany
{
name: 'relationHasManyField',
type: 'relationship',
relationTo: relationSlug,
hasMany: true,
},
// Relation multiple relationTo
{
name: 'relationMultiRelationTo',
type: 'relationship',
relationTo: [relationSlug, 'dummy'],
},
// Relation multiple relationTo hasMany
{
name: 'relationMultiRelationToHasMany',
type: 'relationship',
relationTo: [relationSlug, 'dummy'],
hasMany: true,
type: 'blocks',
name: 'blocks',
required: true,
blocks: [
{
slug: 'block1',
interfaceName: 'SharedMetaBlock',
fields: [
{
required: true,
name: 'b1title',
type: 'text',
},
{
name: 'b1description',
type: 'text',
},
],
},
{
slug: 'block2',
interfaceName: 'AnotherSharedBlock',
fields: [
{
name: 'b2title',
type: 'text',
required: true,
},
{
name: 'b2description',
type: 'text',
},
],
},
],
},
],
},
{
slug: 'collection2',
fields: [
{
type: 'array',
name: 'meta',
interfaceName: 'SharedMetaArray',
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'description',
type: 'text',
},
],
},
{
type: 'group',
name: 'meta',
interfaceName: 'SharedMeta',
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'description',
type: 'text',
},
],
},
{
type: 'group',
name: 'nestedGroup',
fields: [
{
type: 'group',
name: 'meta',
interfaceName: 'SharedMeta',
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'description',
type: 'text',
},
],
},
],
},
],
},
collectionWithName(relationSlug),
collectionWithName('dummy'),
],
});

View File

@@ -0,0 +1,68 @@
/* tslint:disable */
/**
* This file was automatically generated by Payload CMS.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
export type SharedMetaArray = {
title?: string;
description?: string;
id?: string;
}[];
export interface Config {
collections: {
collection1: Collection1;
collection2: Collection2;
users: User;
};
globals: {};
}
export interface Collection1 {
id: string;
testing: string;
title: string;
meta?: SharedMetaArray;
blocks: (SharedMetaBlock | AnotherSharedBlock)[];
updatedAt: string;
createdAt: string;
}
export interface SharedMetaBlock {
b1title: string;
b1description?: string;
id?: string;
blockName?: string;
blockType: 'block1';
}
export interface AnotherSharedBlock {
b2title: string;
b2description?: string;
id?: string;
blockName?: string;
blockType: 'block2';
}
export interface Collection2 {
id: string;
meta?: SharedMeta;
nestedGroup?: {
meta?: SharedMeta;
};
updatedAt: string;
createdAt: string;
}
export interface SharedMeta {
title?: string;
description?: string;
}
export interface User {
id: string;
updatedAt: string;
createdAt: string;
email?: string;
resetPasswordToken?: string;
resetPasswordExpiration?: string;
loginAttempts?: number;
lockUntil?: string;
password?: string;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"esModuleInterop": true,
"paths": {
"payload/generated-types": [
"./payload-types.ts",
],
},
}
}