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:
committed by
Dan Ribbens
parent
a518480292
commit
8458a98eff
@@ -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)
|
||||
|
||||

|
||||
*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")}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
@@ -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`.
|
||||
|
||||

|
||||
*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";
|
||||
```
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
@@ -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*
|
||||
_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,
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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) },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -15,6 +15,9 @@ export default function registerSchema(payload: Payload): void {
|
||||
payload.types = {
|
||||
blockTypes: {},
|
||||
blockInputTypes: {},
|
||||
groupTypes: {},
|
||||
arrayTypes: {},
|
||||
tabTypes: {},
|
||||
};
|
||||
|
||||
if (payload.config.localization) {
|
||||
|
||||
@@ -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;
|
||||
@@ -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] },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'),
|
||||
],
|
||||
});
|
||||
|
||||
68
test/graphql-schema-gen/payload-types.ts
Normal file
68
test/graphql-schema-gen/payload-types.ts
Normal 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;
|
||||
}
|
||||
1901
test/graphql-schema-gen/schema.graphql
Normal file
1901
test/graphql-schema-gen/schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
10
test/graphql-schema-gen/tsconfig.json
Normal file
10
test/graphql-schema-gen/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"paths": {
|
||||
"payload/generated-types": [
|
||||
"./payload-types.ts",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user