diff --git a/docs/authentication/overview.mdx b/docs/authentication/overview.mdx index a5997a4a4..5122c3a2d 100644 --- a/docs/authentication/overview.mdx +++ b/docs/authentication/overview.mdx @@ -36,7 +36,7 @@ Simple example collection: import { CollectionConfig } from 'payload/types'; const Admins: CollectionConfig = { - slug: + slug: 'admins', // highlight-start auth: { tokenExpiration: 7200, // How many seconds to keep the user logged in diff --git a/docs/configuration/collections.mdx b/docs/configuration/collections.mdx index 3553e1af4..1867fbff4 100644 --- a/docs/configuration/collections.mdx +++ b/docs/configuration/collections.mdx @@ -14,7 +14,7 @@ It's often best practice to write your Collections in separate files and then im | Option | Description | | ---------------- | -------------| -| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Collection. | +| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Collection. Must be kebab-case. | | **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. | | **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. | | **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). | diff --git a/docs/configuration/globals.mdx b/docs/configuration/globals.mdx index 6bd5b9487..a1ad179fd 100644 --- a/docs/configuration/globals.mdx +++ b/docs/configuration/globals.mdx @@ -14,7 +14,7 @@ As with Collection configs, it's often best practice to write your Globals in se | Option | Description | | ---------------- | -------------| -| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Global. | +| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Global. Must be kebab-case. | | **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Global. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. | | **`label`** | Singular label for use in identifying this Global throughout Payload. Auto-generated from slug if not defined. | | **`description`**| Text or React component to display below the Global header to give editors more information. | diff --git a/docs/rest-api/overview.mdx b/docs/rest-api/overview.mdx index c6aa962bc..279d627f2 100644 --- a/docs/rest-api/overview.mdx +++ b/docs/rest-api/overview.mdx @@ -64,8 +64,8 @@ Globals cannot be created or deleted, so there are only two REST endpoints opene | Method | Path | Description | | -------- | --------------------------- | ----------------------- | -| `GET` | `/api/globals/{globalSlug}` | Get a global by slug | -| `POST` | `/api/globals/{globalSlug}` | Update a global by slug | +| `GET` | `/api/globals/{global-slug}` | Get a global by slug | +| `POST` | `/api/globals/{global-slug}` | Update a global by slug | ## Preferences diff --git a/docs/versions/overview.mdx b/docs/versions/overview.mdx index 13872c0ec..1b28916e7 100644 --- a/docs/versions/overview.mdx +++ b/docs/versions/overview.mdx @@ -101,9 +101,9 @@ Versions expose new operations for both collections and globals. They allow you | Method | Path | Description | | -------- | ------------------------------------ | -------------------------------------- | -| `GET` | `/api/{collectionSlug}/versions` | Find and query paginated versions | -| `GET` | `/api/{collectionSlug}/versions/:id` | Find a specific version by ID | -| `POST` | `/api/{collectionSlug}/versions/:id` | Restore a version by ID | +| `GET` | `/api/{collection-slug}/versions` | Find and query paginated versions | +| `GET` | `/api/{collection-slug}/versions/:id` | Find a specific version by ID | +| `POST` | `/api/{collection-slug}/versions/:id` | Restore a version by ID | **Collection GraphQL queries:** @@ -174,9 +174,9 @@ const result = await payload.restoreVersion({ | Method | Path | Description | | -------- | ---------------------------------------- | -------------------------------------- | -| `GET` | `/api/globals/{globalSlug}/versions` | Find and query paginated versions | -| `GET` | `/api/globals/{globalSlug}/versions/:id` | Find a specific version by ID | -| `POST` | `/api/globals/{globalSlug}/versions/:id` | Restore a version by ID | +| `GET` | `/api/globals/{global-slug}/versions` | Find and query paginated versions | +| `GET` | `/api/globals/{global-slug}/versions/:id` | Find a specific version by ID | +| `POST` | `/api/globals/{global-slug}/versions/:id` | Restore a version by ID | **Global GraphQL queries:** diff --git a/src/collections/config/sanitize.ts b/src/collections/config/sanitize.ts index 4fed5c906..1817918df 100644 --- a/src/collections/config/sanitize.ts +++ b/src/collections/config/sanitize.ts @@ -15,6 +15,7 @@ import { versionCollectionDefaults } from '../../versions/defaults'; import baseVersionFields from '../../versions/baseFields'; import TimestampsRequired from '../../errors/TimestampsRequired'; import mergeBaseFields from '../../fields/mergeBaseFields'; +import Logger from '../../utilities/logger'; const sanitizeCollection = (config: Config, collection: CollectionConfig): SanitizedCollectionConfig => { // ///////////////////////////////// @@ -25,6 +26,15 @@ const sanitizeCollection = (config: Config, collection: CollectionConfig): Sanit isMergeableObject: isPlainObject, }); + const logger = Logger(); + + // Pre-validate slug to enforce kebab-case + if (!sanitized.slug.match(new RegExp('^([a-z][a-z0-9]*)(-[a-z0-9]+)*$'))) { + logger.error(`Collection slug "${sanitized.slug}" should be kebab-case.`); + process.exit(1); + } + + // Normalize slug and labels to kebab-case sanitized.slug = toKebabCase(sanitized.slug); sanitized.labels = sanitized.labels || formatLabels(sanitized.slug); diff --git a/src/collections/config/types.ts b/src/collections/config/types.ts index 3341c996d..97c7cc2e0 100644 --- a/src/collections/config/types.ts +++ b/src/collections/config/types.ts @@ -204,6 +204,9 @@ export type CollectionAdminOptions = { } export type CollectionConfig = { + /** + * Name of the collection in kebab-case + */ slug: string; /** * Label configuration diff --git a/src/globals/config/sanitize.ts b/src/globals/config/sanitize.ts index bd70787a5..997d9acf9 100644 --- a/src/globals/config/sanitize.ts +++ b/src/globals/config/sanitize.ts @@ -7,11 +7,20 @@ import defaultAccess from '../../auth/defaultAccess'; import baseVersionFields from '../../versions/baseFields'; import mergeBaseFields from '../../fields/mergeBaseFields'; import { versionGlobalDefaults } from '../../versions/defaults'; +import Logger from '../../utilities/logger'; const sanitizeGlobals = (collections: CollectionConfig[], globals: GlobalConfig[]): SanitizedGlobalConfig[] => { const sanitizedGlobals = globals.map((global) => { const sanitizedGlobal = { ...global }; + const logger = Logger(); + + // Pre-validate slug to enforce kebab-case + if (!sanitizedGlobal.slug.match(new RegExp('^([a-z][a-z0-9]*)(-[a-z0-9]+)*$'))) { + logger.error(`Global slug "${sanitizedGlobal.slug}" should be kebab-case.`); + process.exit(1); + } + sanitizedGlobal.label = sanitizedGlobal.label || toWords(sanitizedGlobal.slug); // ///////////////////////////////// diff --git a/src/globals/config/types.ts b/src/globals/config/types.ts index 14a14a055..d4a26c467 100644 --- a/src/globals/config/types.ts +++ b/src/globals/config/types.ts @@ -45,6 +45,9 @@ export interface GlobalModel extends Model { } export type GlobalConfig = { + /** + * Name of the global in kebab-case + */ slug: string label?: string preview?: GeneratePreviewURL