diff --git a/package.json b/package.json index 7a1c7b79e7..265e79367e 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "release:minor": "release-it minor", "release:major": "release-it major", "release:beta": "release-it prepatch --config .release-it.beta.json", + "fix": "eslint \"src/**/*.ts\" --fix", "lint": "eslint \"src/**/*.ts\"" }, "bugs": { diff --git a/src/admin/components/forms/Form/context.ts b/src/admin/components/forms/Form/context.ts index c49b450a81..880b9f5971 100644 --- a/src/admin/components/forms/Form/context.ts +++ b/src/admin/components/forms/Form/context.ts @@ -9,13 +9,30 @@ const ProcessingContext = createContext(false); const ModifiedContext = createContext(false); const FormFieldsContext = createSelectorContext([{}, () => null]); +/** + * Get the state of the form, can be used to submit & validate the form. + * + * @see https://payloadcms.com/docs/admin/hooks#useform + */ const useForm = (): Context => useContext(FormContext); const useWatchForm = (): Context => useContext(FormWatchContext); const useFormSubmitted = (): boolean => useContext(SubmittedContext); const useFormProcessing = (): boolean => useContext(ProcessingContext); const useFormModified = (): boolean => useContext(ModifiedContext); + +/** + * Get and set the value of a form field based on a selector + * + * @see https://payloadcms.com/docs/admin/hooks#useformfields + */ const useFormFields = (selector: (context: FormFieldsContextType) => Value): Value => useContextSelector(FormFieldsContext, selector); + +/** + * Get the state of all form fields. + * + * @see https://payloadcms.com/docs/admin/hooks#useallformfields + */ const useAllFormFields = (): FormFieldsContextType => useFullContext(FormFieldsContext); export { diff --git a/src/admin/components/forms/useField/index.tsx b/src/admin/components/forms/useField/index.tsx index 0a23565622..21e87d8a40 100644 --- a/src/admin/components/forms/useField/index.tsx +++ b/src/admin/components/forms/useField/index.tsx @@ -8,6 +8,11 @@ import { useOperation } from '../../utilities/OperationProvider'; import useThrottledEffect from '../../../hooks/useThrottledEffect'; import { UPDATE } from '../Form/types'; +/** + * Get and set the value of a form field. + * + * @see https://payloadcms.com/docs/admin/hooks#usefield + */ const useField = (options: Options): FieldType => { const { path, diff --git a/src/collections/config/types.ts b/src/collections/config/types.ts index 9de6e1e161..620e092991 100644 --- a/src/collections/config/types.ts +++ b/src/collections/config/types.ts @@ -203,6 +203,7 @@ export type CollectionAdminOptions = { preview?: GeneratePreviewURL } +/** Manage all aspects of a data collection */ export type CollectionConfig = { slug: string; /** @@ -276,10 +277,22 @@ export type CollectionConfig = { */ auth?: IncomingAuthType | boolean; /** - * Upload options + * Customize the handling of incoming file uploads + * + * @default false // disable uploads */ upload?: IncomingUploadType | boolean; + /** + * Customize the handling of incoming file uploads + * + * @default false // disable versioning + */ versions?: IncomingCollectionVersions | boolean; + /** + * Add `createdAt` and `updatedAt` fields + * + * @default true + */ timestamps?: boolean }; diff --git a/src/config/types.ts b/src/config/types.ts index 8962cef638..06d69c4158 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -10,7 +10,11 @@ import React from 'react'; import { LoggerOptions } from 'pino'; import type { InitOptions as i18nInitOptions } from 'i18next'; import { Payload } from '..'; -import { AfterErrorHook, CollectionConfig, SanitizedCollectionConfig } from '../collections/config/types'; +import { + AfterErrorHook, + CollectionConfig, + SanitizedCollectionConfig, +} from '../collections/config/types'; import { GlobalConfig, SanitizedGlobalConfig } from '../globals/config/types'; import { PayloadRequest } from '../express/types'; import { Where } from '../types'; @@ -20,17 +24,20 @@ type Email = { fromName: string; fromAddress: string; logMockCredentials?: boolean; -} +}; // eslint-disable-next-line no-use-before-define export type Plugin = (config: Config) => Config; type GeneratePreviewURLOptions = { - locale: string - token: string -} + locale: string; + token: string; +}; -export type GeneratePreviewURL = (doc: Record, options: GeneratePreviewURLOptions) => Promise | string +export type GeneratePreviewURL = ( + doc: Record, + options: GeneratePreviewURLOptions +) => Promise | string; export type EmailTransport = Email & { transport: Transporter; @@ -48,7 +55,9 @@ export type EmailOptions = EmailTransport | EmailTransportOptions | Email; * type guard for EmailOptions * @param emailConfig */ -export function hasTransport(emailConfig: EmailOptions): emailConfig is EmailTransport { +export function hasTransport( + emailConfig: EmailOptions, +): emailConfig is EmailTransport { return (emailConfig as EmailTransport).transport !== undefined; } @@ -56,15 +65,18 @@ export function hasTransport(emailConfig: EmailOptions): emailConfig is EmailTra * type guard for EmailOptions * @param emailConfig */ -export function hasTransportOptions(emailConfig: EmailOptions): emailConfig is EmailTransportOptions { +export function hasTransportOptions( + emailConfig: EmailOptions, +): emailConfig is EmailTransportOptions { return (emailConfig as EmailTransportOptions).transportOptions !== undefined; } - export type InitOptions = { /** Express app for Payload to use */ express?: Express; + /** Mongo connection URL, starts with `mongo` */ mongoURL: string | false; + /** Extra configuration options that will be passed to Mongo */ mongoOptions?: ConnectOptions; /** Secure string that Payload will use for any encryption workflows */ @@ -73,7 +85,7 @@ export type InitOptions = { /** * Configuration for Payload's email functionality * - * https://payloadcms.com/docs/email/overview + * @see https://payloadcms.com/docs/email/overview */ email?: EmailOptions; @@ -98,158 +110,442 @@ export type InitOptions = { loggerOptions?: LoggerOptions; }; +/** + * This result is calculated on the server + * and then sent to the client allowing the dashboard to show accessible data and actions. + * + * If the result is `true`, the user has access. + * If the result is an object, it is interpreted as a Mongo query. + * + * @example `{ createdBy: { equals: id } }` + * + * @example `{ tenant: { in: tenantIds } }` + * + * @see https://payloadcms.com/docs/access-control/overview + */ export type AccessResult = boolean | Where; type AccessArgs = { - req: PayloadRequest - id?: string | number - data?: T + /** The original request that requires an access check */ + req: PayloadRequest; + /** ID of the resource being accessed */ + id?: string | number; + /** + * The relevant resource that is being accessed. + * + * `data` is null when a list is requested + */ + data?: T; +}; + +/** + * Access function runs on the server + * and is sent to the client allowing the dashboard to show accessible data and actions. + * + * @see https://payloadcms.com/docs/access-control/overview + */ +export type Access = ( + args: AccessArgs +) => AccessResult | Promise; + +/** Equivalent to express middleware, but with an enhanced request object */ +export interface PayloadHandler { + (req: PayloadRequest, res: Response, next: NextFunction): void; } /** - * Access function + * Docs: https://payloadcms.com/docs/rest-api/overview#custom-endpoints */ -export type Access = (args: AccessArgs) => AccessResult | Promise; - -export interface PayloadHandler { - ( - req: PayloadRequest, - res: Response, - next: NextFunction, - ): void -} - export type Endpoint = { - path: string - method: 'get' | 'head' | 'post' | 'put' | 'patch' | 'delete' | 'connect' | 'options' | string - handler: PayloadHandler | PayloadHandler[] - root?: boolean -} + /** + * Pattern that should match the path of the incoming request + * + * Compatible with the Express router + */ + path: string; + /** HTTP method (or "all") */ + method: + | 'get' + | 'head' + | 'post' + | 'put' + | 'patch' + | 'delete' + | 'connect' + | 'options' + | string; + /** + * Middleware that will be called when the path/method matches + * + * Compatible with Express middleware + */ + handler: PayloadHandler | PayloadHandler[]; + /** + * Set to `true` to disable the Payload middleware for this endpoint + * @default false + */ + root?: boolean; +}; -export type AdminView = React.ComponentType<{ user: User, canAccessAdmin: boolean }> +export type AdminView = React.ComponentType<{ + user: User; + canAccessAdmin: boolean; +}>; export type AdminRoute = { - Component: AdminView - path: string - exact?: boolean - strict?: boolean - sensitive?: boolean -} + Component: AdminView; + path: string; + /** Whether the path should be matched exactly or as a prefix */ + exact?: boolean; + strict?: boolean; + sensitive?: boolean; +}; +/** + * @see https://payloadcms.com/docs/configuration/localization#localization + */ export type LocalizationConfig = { - locales: string[] - defaultLocale: string - fallback?: boolean -} + /** + * List of supported locales + * @exanple `["en", "es", "fr", "nl", "de", "jp"]` + */ + locales: string[]; + /** + * Locale for users that have not expressed their preference for a specific locale + * @exanple `"en"` + */ + defaultLocale: string; + /** Set to `true` to let missing values in localised fields fall back to the values in `defaultLocale` */ + fallback?: boolean; +}; +/** + * This is the central configuration + * + * @see https://payloadcms.com/docs/configuration/overview + */ export type Config = { + /** Configure admin dashboard */ admin?: { + /** The slug of a Collection that you want be used to log in to the Admin dashboard. */ user?: string; + /** Base meta data to use for the Admin panel. Included properties are titleSuffix, ogImage, and favicon. */ meta?: { + /** + * String to append to the of admin pages + * @example `" - My Brand"` + */ titleSuffix?: string; + /** + * Public path to an image + * + * This image may be displayed as preview when the link is shared on social media + */ ogImage?: string; + /** + * Public path to an icon + * + * This image may be displayed in the browser next to the title of the page + */ favicon?: string; - } + }; + /** If set to true, the entire Admin panel will be disabled. */ disable?: boolean; + /** Replace the entirety of the index.html file used by the Admin panel. Reference the base index.html file to ensure your replacement has the appropriate HTML elements. */ indexHTML?: string; - css?: string - dateFormat?: string - avatar?: 'default' | 'gravatar' | React.ComponentType<any>, - logoutRoute?: string, - inactivityRoute?: string, + /** Absolute path to a stylesheet that you can use to override / customize the Admin panel styling. */ + css?: string; + /** Global date format that will be used for all dates in the Admin panel. Any valid date-fns format pattern can be used. */ + dateFormat?: string; + /** Set account profile picture. Options: gravatar, default or a custom React component. */ + avatar?: 'default' | 'gravatar' | React.ComponentType<any>; + /** The route for the logout page. */ + logoutRoute?: string; + /** The route the user will be redirected to after being inactive for too long. */ + inactivityRoute?: string; + /** + * Add extra and/or replace built-in components with custom components + * + * @see https://payloadcms.com/docs/admin/components + */ components?: { - routes?: AdminRoute[] - providers?: React.ComponentType<{ children: React.ReactNode }>[] - beforeDashboard?: React.ComponentType<any>[] - afterDashboard?: React.ComponentType<any>[] - beforeLogin?: React.ComponentType<any>[] - afterLogin?: React.ComponentType<any>[] - beforeNavLinks?: React.ComponentType<any>[] - afterNavLinks?: React.ComponentType<any>[] - Nav?: React.ComponentType<any> + /** + * Add custom routes in the admin dashboard + */ + routes?: AdminRoute[]; + /** + * Wrap the admin dashboard in custom context providers + */ + providers?: React.ComponentType<{ children: React.ReactNode }>[]; + /** + * Add custom components before the collection overview + */ + beforeDashboard?: React.ComponentType<any>[]; + /** + * Add custom components after the collection overview + */ + afterDashboard?: React.ComponentType<any>[]; + /** + * Add custom components before the email/password field + */ + beforeLogin?: React.ComponentType<any>[]; + /** + * Add custom components after the email/password field + */ + afterLogin?: React.ComponentType<any>[]; + /** + * Add custom components before the navigation links + */ + beforeNavLinks?: React.ComponentType<any>[]; + /** + * Add custom components after the navigation links + */ + afterNavLinks?: React.ComponentType<any>[]; + /** + * Replace the navigation with a custom component + */ + Nav?: React.ComponentType<any>; + /** Replace logout related components */ logout?: { - Button?: React.ComponentType<any>, - } + /** Replace the logout button */ + Button?: React.ComponentType<any>; + }; + /** Replace graphical components */ graphics?: { - Icon?: React.ComponentType<any> - Logo?: React.ComponentType<any> - } + /** Replace the icon in the navigation */ + Icon?: React.ComponentType<any>; + /** Replace the logo on the login page */ + Logo?: React.ComponentType<any>; + v; + }; + /* Replace complete pages */ views?: { - Account?: React.ComponentType<any> - Dashboard?: React.ComponentType<any> - } - } + /** Replace the account screen */ + Account?: React.ComponentType<any>; + /** Replace the admin homepage */ + Dashboard?: React.ComponentType<any>; + v; + }; + }; + /** + * Control pagination when querying collections. + * + * @see https://payloadcms.com/docs/queries/overview + */ pagination?: { + /** + * Limit the number of documents that are displayed on 1 page in the list view + * + * @default 10 + */ defaultLimit?: number; - options?: number[]; - } + /** + * Suggest alternative options for the limit of documents on the list view + * + * @default [5, 10, 25, 50, 100] + */ + limits?: number[] + }; + /** Customize the Webpack config that's used to generate the Admin panel. */ webpack?: (config: Configuration) => Configuration; }; + /** + * Manage the datamodel of your application + * + * @see https://payloadcms.com/docs/configuration/collections#collection-configs + */ collections?: CollectionConfig[]; + /** Custom REST endpoints */ endpoints?: Endpoint[]; + /** + * @see https://payloadcms.com/docs/configuration/globals#global-configs + */ globals?: GlobalConfig[]; + /** + * Control the behaviour of the admin internationalisation. + * + * See i18next options. + * + * @default + * { + * fallbackLng: 'en', + * debug: false, + * supportedLngs: Object.keys(translations), + * resources: translations, + * } + */ i18n?: i18nInitOptions; + /** + * Define the absolute URL of your app including the protocol, for example `https://example.org`. + * No paths allowed, only protocol, domain and (optionally) port. + * + * @see https://payloadcms.com/docs/configuration/overview#options + */ serverURL?: string; + /** + * Prefix a string to all cookies that Payload sets. + * + * @default "payload" + */ cookiePrefix?: string; + /** A whitelist array of URLs to allow Payload cookies to be accepted from as a form of CSRF protection. */ csrf?: string[]; + /** Either a whitelist array of URLS to allow CORS requests from, or a wildcard string ('*') to accept incoming requests from any domain. */ cors?: string[] | '*'; + /** Control the routing structure that Payload binds itself to. */ routes?: { + /** Defaults to /api */ api?: string; + /** Defaults to /admin */ admin?: string; + /** Defaults to /graphql */ graphQL?: string; + /** Defaults to /playground */ graphQLPlayground?: string; }; + /** Control how typescript interfaces are generated from your collections. */ typescript?: { - outputFile?: string - } - debug?: boolean + /** Filename to write the generated types to */ + outputFile?: string; + }; + /** Enable to expose more detailed error information. */ + debug?: boolean; + /** + * Express-specific middleware options such as compression and JSON parsing. + * + * @see https://payloadcms.com/docs/configuration/express + */ express?: { + /** Control the way JSON is parsed */ json?: { - limit?: number - }, + /** Defaults to 2MB */ + limit?: number; + }; + /** Control the way responses are compressed */ compression?: { - [key: string]: unknown - }, + [key: string]: unknown; + }; /** * @deprecated express.middleware will be removed in a future version. Please migrate to express.postMiddleware. */ - middleware?: any[] - preMiddleware?: any[] - postMiddleware?: any[] - }, + middleware?: any[]; + preMiddleware?: any[]; + postMiddleware?: any[]; + }; + /** + * If a user does not specify `depth` while requesting a resource, this depth will be used. + * + * @see https://payloadcms.com/docs/getting-started/concepts#depth + * + * @default 2 + */ defaultDepth?: number; + /** + * The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. + * + * @see https://payloadcms.com/docs/getting-started/concepts#depth + * + * @default 10 + */ maxDepth?: number; + /** + * The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. + * + * @default 40000 + */ defaultMaxTextLength?: number; + /** Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. */ indexSortableFields?: boolean; + /** + * Limit heavy usage + * + * @default + * { + * window: 15 * 60 * 100, // 1.5 minutes, + * max: 500, + * } + */ rateLimit?: { window?: number; max?: number; trustProxy?: boolean; skip?: (req: PayloadRequest) => boolean; }; + /** + * Customize the handling of incoming file uploads for collections that have uploads enabled. + */ upload?: Options; + /** + * Translate your content to different languages/locales. + * + * @default false // disable localization + */ localization?: LocalizationConfig | false; + /** + * Manage the GraphQL API + * + * You can add your own GraphQL queries and mutations to Payload, making use of all the types that Payload has defined for you. + * + * @see https://payloadcms.com/docs/access-control/overview + */ graphQL?: { - mutations?: ((graphQL: typeof GraphQL, payload: Payload) => Record<string, unknown>), - queries?: ((graphQL: typeof GraphQL, payload: Payload) => Record<string, unknown>), + /** + * Function that returns an object containing keys to custom GraphQL mutations + * + * @see https://payloadcms.com/docs/access-control/overview + */ + mutations?: ( + graphQL: typeof GraphQL, + payload: Payload + ) => Record<string, unknown>; + /** + * Function that returns an object containing keys to custom GraphQL queries + * + * @see https://payloadcms.com/docs/access-control/overview + */ + queries?: ( + graphQL: typeof GraphQL, + payload: Payload + ) => Record<string, unknown>; maxComplexity?: number; disablePlaygroundInProduction?: boolean; disable?: boolean; schemaOutputFile?: string; }; + /** + * Replace the built-in components with custom ones + */ components?: { [key: string]: JSX.Element | (() => JSX.Element) }; + /** + * Tap into Payload-wide hooks. + * + * @see https://payloadcms.com/docs/hooks/overview + */ hooks?: { afterError?: AfterErrorHook; }; + /** + * An array of Payload plugins. + * + * @see https://payloadcms.com/docs/plugins/overview + */ plugins?: Plugin[]; + /** Send anonymous telemetry data about general usage. */ telemetry?: boolean; - onInit?: (payload: Payload) => Promise<void> | void + /** A function that is called immediately following startup that receives the Payload instance as its only argument. */ + onInit?: (payload: Payload) => Promise<void> | void; }; -export type SanitizedConfig = Omit<DeepRequired<Config>, 'collections' | 'globals'> & { - collections: SanitizedCollectionConfig[] - globals: SanitizedGlobalConfig[] +export type SanitizedConfig = Omit< + DeepRequired<Config>, + 'collections' | 'globals' +> & { + collections: SanitizedCollectionConfig[]; + globals: SanitizedGlobalConfig[]; paths: { [key: string]: string }; -} +}; -export type EntityDescription = string | Record<string, string> | (() => string) | React.ComponentType<any> +export type EntityDescription = + | string + | (() => string) + | React.ComponentType<any>; diff --git a/src/express/types.ts b/src/express/types.ts index a33b325361..8e363fcc60 100644 --- a/src/express/types.ts +++ b/src/express/types.ts @@ -8,20 +8,44 @@ import { User } from '../auth/types'; import { Document } from '../types'; import { TypeWithID } from '../globals/config/types'; -export declare type PayloadRequest<T = any> = Request & { +/** Express request with some Payload related context added */ +export declare type PayloadRequest<U = any> = Request & { + /** The global payload object */ payload: Payload; + /** Optimized document loader */ payloadDataLoader: DataLoader<string, TypeWithID>; + /** + * The requested locale if specified + * Only available for localised collections + */ locale?: string; + /** The locale that should be used for a field when it is not translated to the requested locale */ fallbackLocale?: string; + /** Information about the collection that is being accessed + * - Configuration from payload-config.ts + * - Mongo model for this collection + * - GraphQL type metadata + * */ collection?: Collection; + /** What triggered this request */ payloadAPI: 'REST' | 'local' | 'graphQL'; + /** Uploaded files */ files?: { + /** + * This is the file that Payload will use for the file upload, other files are ignored. + * + */ file: UploadedFile; }; + /** I18next instance */ i18n: Ii18n; + /** Get a translation for the admin screen */ t: TFunction; - user: T & User | null; + /** The signed in user */ + user: (U & User) | null; + /** Resized versions of the image that was uploaded during this request */ payloadUploadSizes?: Record<string, Buffer>; + /** Cache of documents related to the current request */ findByID?: { [slug: string]: (q: unknown) => Document; }; diff --git a/src/fields/config/sanitize.spec.ts b/src/fields/config/sanitize.spec.ts index b615f50239..87102a6704 100644 --- a/src/fields/config/sanitize.spec.ts +++ b/src/fields/config/sanitize.spec.ts @@ -16,13 +16,13 @@ describe('sanitizeFields', () => { sanitizeFields(fields, []); }).toThrow(MissingFieldType); }); - it("should throw on invalid field name", () => { + it('should throw on invalid field name', () => { const fields: Field[] = [ { - label: "some.collection", - name: "some.collection", - type: "text", - } + label: 'some.collection', + name: 'some.collection', + type: 'text', + }, ]; expect(() => { sanitizeFields(fields, []); diff --git a/src/types/index.ts b/src/types/index.ts index 405bf595e4..69487d236d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -16,28 +16,28 @@ export type Operator = | 'less_than' | 'less_than_equal' | 'like' - | 'near' + | 'near'; export type WhereField = { - [key in Operator]?: unknown -} + [key in Operator]?: unknown; +}; export type Where = { - [key: string]: WhereField | Where[] - or?: Where[] - and?: Where[] -} + [key: string]: WhereField | Where[]; + or?: Where[]; + and?: Where[]; +}; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Document = any; export interface PayloadMongooseDocument extends MongooseDocument { - setLocale: (locale: string, fallback: string) => void - filename?: string - sizes?: FileData[] + setLocale: (locale: string, fallback: string) => void; + filename?: string; + sizes?: FileData[]; } -export type Operation = 'create' | 'read' | 'update' | 'delete' +export type Operation = 'create' | 'read' | 'update' | 'delete'; export function docHasTimestamps(doc: any): doc is TypeWithTimestamps { return doc?.createdAt && doc?.updatedAt; diff --git a/src/uploads/imageResizer.ts b/src/uploads/imageResizer.ts index 4f90cc9397..d336092509 100644 --- a/src/uploads/imageResizer.ts +++ b/src/uploads/imageResizer.ts @@ -115,7 +115,7 @@ export default async function resizeAndSave({ function createImageName( outputImage: OutputImage, bufferObject: { data: Buffer; info: sharp.OutputInfo }, - extension: string + extension: string, ): string { return `${outputImage.name}-${bufferObject.info.width}x${bufferObject.info.height}.${extension}`; } diff --git a/src/utilities/arrayMove.ts b/src/utilities/arrayMove.ts index a6c694eedb..ece8ad5887 100644 --- a/src/utilities/arrayMove.ts +++ b/src/utilities/arrayMove.ts @@ -3,7 +3,7 @@ export function arrayMove<T>(array: readonly T[], from: number, to: number) { slicedArray.splice( to < 0 ? array.length + to : to, 0, - slicedArray.splice(from, 1)[0] + slicedArray.splice(from, 1)[0], ); return slicedArray; }