From 6a1a83cc2b4b91945627e8635c2ac16647e9fc0b Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 15 Feb 2024 14:57:53 -0500 Subject: [PATCH 1/5] feat: passing auth test suite --- packages/db-mongodb/package.json | 1 + .../src/queries/buildSearchParams.ts | 4 ++-- packages/graphql/src/resolvers/auth/login.ts | 4 ++++ packages/graphql/src/resolvers/auth/me.ts | 8 +++++++- .../graphql/src/resolvers/auth/refresh.ts | 5 +++++ .../src/resolvers/auth/resetPassword.ts | 5 +++++ packages/next/src/routes/rest/auth/login.ts | 8 +++++--- packages/next/src/routes/rest/auth/me.ts | 4 ++++ packages/next/src/routes/rest/auth/refresh.ts | 8 +++++--- .../src/routes/rest/auth/resetPassword.ts | 7 +++++-- packages/payload/package.json | 2 +- .../src/auth/operations/local/login.ts | 8 +++++++- .../auth/operations/local/resetPassword.ts | 8 +++++++- packages/payload/src/auth/operations/login.ts | 4 ---- packages/payload/src/auth/operations/me.ts | 2 +- .../payload/src/auth/operations/refresh.ts | 4 ---- .../src/auth/operations/resetPassword.ts | 6 ++++-- .../src/fields/baseFields/baseIDField.ts | 4 ++-- .../src/preferences/requestHandlers/update.ts | 2 +- packages/payload/src/utilities/isValidID.ts | 4 ++-- packages/ui/package.json | 2 +- packages/ui/src/forms/Form/fieldReducer.ts | 10 +++++----- .../addFieldStatePromise.ts | 6 +++--- pnpm-lock.yaml | 20 +++++++++++++------ test/auth/int.spec.ts | 17 ++++------------ 25 files changed, 95 insertions(+), 58 deletions(-) diff --git a/packages/db-mongodb/package.json b/packages/db-mongodb/package.json index 49cba0f6e5..a37f1d4bad 100644 --- a/packages/db-mongodb/package.json +++ b/packages/db-mongodb/package.json @@ -27,6 +27,7 @@ "prepublishOnly": "pnpm clean && pnpm build" }, "dependencies": { + "bson": "^6.3.0", "bson-ext": "^4.0.3", "bson-objectid": "2.0.4", "deepmerge": "4.3.1", diff --git a/packages/db-mongodb/src/queries/buildSearchParams.ts b/packages/db-mongodb/src/queries/buildSearchParams.ts index 1ba2fd03c1..0af5634bee 100644 --- a/packages/db-mongodb/src/queries/buildSearchParams.ts +++ b/packages/db-mongodb/src/queries/buildSearchParams.ts @@ -2,7 +2,7 @@ import type { PathToQuery } from 'payload/database' import type { Field, Payload } from 'payload/types' import type { Operator } from 'payload/types' -import objectID from 'bson-objectid' +import { ObjectId } from 'bson' import mongoose from 'mongoose' import { getLocalizedPaths } from 'payload/database' import { fieldAffectsData } from 'payload/types' @@ -196,7 +196,7 @@ export async function buildSearchParam({ if (typeof formattedValue === 'string') { if (mongoose.Types.ObjectId.isValid(formattedValue)) { - result.value.$or.push({ [path]: { [operatorKey]: objectID(formattedValue) } }) + result.value.$or.push({ [path]: { [operatorKey]: new ObjectId(formattedValue) } }) } else { ;(Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]).forEach( (relationTo) => { diff --git a/packages/graphql/src/resolvers/auth/login.ts b/packages/graphql/src/resolvers/auth/login.ts index 564475b054..6e28b5a3bf 100644 --- a/packages/graphql/src/resolvers/auth/login.ts +++ b/packages/graphql/src/resolvers/auth/login.ts @@ -26,6 +26,10 @@ function loginResolver(collection: Collection) { context.headers['Set-Cookie'] = cookie + if (collection.config.auth.removeTokenFromResponses) { + delete result.token + } + return result } diff --git a/packages/graphql/src/resolvers/auth/me.ts b/packages/graphql/src/resolvers/auth/me.ts index 03a880bf63..5facc0af09 100644 --- a/packages/graphql/src/resolvers/auth/me.ts +++ b/packages/graphql/src/resolvers/auth/me.ts @@ -11,7 +11,13 @@ function meResolver(collection: Collection): any { depth: 0, req: isolateTransactionID(context.req), } - return meOperation(options) + const result = await meOperation(options) + + if (collection.config.auth.removeTokenFromResponses) { + delete result.token + } + + return result } return resolver diff --git a/packages/graphql/src/resolvers/auth/refresh.ts b/packages/graphql/src/resolvers/auth/refresh.ts index 3562a38505..b482d40151 100644 --- a/packages/graphql/src/resolvers/auth/refresh.ts +++ b/packages/graphql/src/resolvers/auth/refresh.ts @@ -29,6 +29,11 @@ function refreshResolver(collection: Collection) { collectionConfig: collection.config, }) context.headers['Set-Cookie'] = cookie + + if (collection.config.auth.removeTokenFromResponses) { + delete result.refreshedToken + } + return result } diff --git a/packages/graphql/src/resolvers/auth/resetPassword.ts b/packages/graphql/src/resolvers/auth/resetPassword.ts index dcc4cb9a0f..7e15777087 100644 --- a/packages/graphql/src/resolvers/auth/resetPassword.ts +++ b/packages/graphql/src/resolvers/auth/resetPassword.ts @@ -25,6 +25,11 @@ function resetPasswordResolver(collection: Collection) { collectionConfig: collection.config, }) context.headers['Set-Cookie'] = cookie + + if (collection.config.auth.removeTokenFromResponses) { + delete result.token + } + return result } diff --git a/packages/next/src/routes/rest/auth/login.ts b/packages/next/src/routes/rest/auth/login.ts index 8b780ab8d5..5a409a0d27 100644 --- a/packages/next/src/routes/rest/auth/login.ts +++ b/packages/next/src/routes/rest/auth/login.ts @@ -24,13 +24,15 @@ export const login: CollectionRouteHandler = async ({ req, collection }) => { collectionConfig: collection.config, }) + if (collection.config.auth.removeTokenFromResponses) { + delete result.token + } + return Response.json( { - exp: result.exp, // TODO(translate) message: 'Auth Passed', - token: result.token, - user: result.user, + ...result, }, { headers: new Headers({ diff --git a/packages/next/src/routes/rest/auth/me.ts b/packages/next/src/routes/rest/auth/me.ts index 82f07015c3..c9a609fb5e 100644 --- a/packages/next/src/routes/rest/auth/me.ts +++ b/packages/next/src/routes/rest/auth/me.ts @@ -12,6 +12,10 @@ export const me: CollectionRouteHandler = async ({ req, collection }) => { currentToken, }) + if (collection.config.auth.removeTokenFromResponses) { + delete result.token + } + return Response.json( { ...result, diff --git a/packages/next/src/routes/rest/auth/refresh.ts b/packages/next/src/routes/rest/auth/refresh.ts index 9ec1e0814d..765e0b9fde 100644 --- a/packages/next/src/routes/rest/auth/refresh.ts +++ b/packages/next/src/routes/rest/auth/refresh.ts @@ -31,13 +31,15 @@ export const refresh: CollectionRouteHandler = async ({ req, collection }) => { collectionConfig: collection.config, }) + if (collection.config.auth.removeTokenFromResponses) { + delete result.refreshedToken + } + return Response.json( { - exp: result.exp, // TODO(translate) message: 'Token refresh successful', - token: result.refreshedToken, - user: result.user, + ...result, }, { headers: new Headers({ diff --git a/packages/next/src/routes/rest/auth/resetPassword.ts b/packages/next/src/routes/rest/auth/resetPassword.ts index 343245a213..63a1d74dea 100644 --- a/packages/next/src/routes/rest/auth/resetPassword.ts +++ b/packages/next/src/routes/rest/auth/resetPassword.ts @@ -24,12 +24,15 @@ export const resetPassword: CollectionRouteHandler = async ({ req, collection }) collectionConfig: collection.config, }) + if (collection.config.auth.removeTokenFromResponses) { + delete result.token + } + return Response.json( { // TODO(translate) message: 'Password reset successfully.', - token: result.token, - user: result.user, + ...result, }, { headers: new Headers({ diff --git a/packages/payload/package.json b/packages/payload/package.json index 5abf39824e..0253ee4718 100644 --- a/packages/payload/package.json +++ b/packages/payload/package.json @@ -48,7 +48,7 @@ "dependencies": { "@payloadcms/graphql": "workspace:^", "@payloadcms/translations": "workspace:^", - "bson-objectid": "2.0.4", + "bson": "^6.3.0", "conf": "10.2.0", "console-table-printer": "2.11.2", "dataloader": "2.2.2", diff --git a/packages/payload/src/auth/operations/local/login.ts b/packages/payload/src/auth/operations/local/login.ts index e2280b681e..6adda48a07 100644 --- a/packages/payload/src/auth/operations/local/login.ts +++ b/packages/payload/src/auth/operations/local/login.ts @@ -51,7 +51,13 @@ async function localLogin( showHiddenFields, } - return loginOperation(args) + const result = await loginOperation(args) + + if (collection.config.auth.removeTokenFromResponses) { + delete result.token + } + + return result } export default localLogin diff --git a/packages/payload/src/auth/operations/local/resetPassword.ts b/packages/payload/src/auth/operations/local/resetPassword.ts index f54617c2c1..793b7890eb 100644 --- a/packages/payload/src/auth/operations/local/resetPassword.ts +++ b/packages/payload/src/auth/operations/local/resetPassword.ts @@ -34,12 +34,18 @@ async function localResetPassword ) } - return resetPasswordOperation({ + const result = await resetPasswordOperation({ collection, data, overrideAccess, req: await createLocalReq(options, payload), }) + + if (collection.config.auth.removeTokenFromResponses) { + delete result.token + } + + return result } export default localResetPassword diff --git a/packages/payload/src/auth/operations/login.ts b/packages/payload/src/auth/operations/login.ts index f51b85501b..d63cc9115f 100644 --- a/packages/payload/src/auth/operations/login.ts +++ b/packages/payload/src/auth/operations/login.ts @@ -230,10 +230,6 @@ export const loginOperation = async // Return results // ///////////////////////////////////// - if (collectionConfig.auth.removeTokenFromResponses) { - delete result.refreshedToken - } - return result } diff --git a/packages/payload/src/auth/operations/resetPassword.ts b/packages/payload/src/auth/operations/resetPassword.ts index 0749bafa05..54758ca520 100644 --- a/packages/payload/src/auth/operations/resetPassword.ts +++ b/packages/payload/src/auth/operations/resetPassword.ts @@ -105,10 +105,12 @@ export const resetPasswordOperation = async (args: Arguments): Promise = }) if (shouldCommit) await commitTransaction(req) - return { - token: collectionConfig.auth.removeTokenFromResponses ? undefined : token, + const result = { + token, user: fullUser, } + + return result } catch (error: unknown) { await killTransaction(req) throw error diff --git a/packages/payload/src/fields/baseFields/baseIDField.ts b/packages/payload/src/fields/baseFields/baseIDField.ts index 92f4e22553..a27c379f2b 100644 --- a/packages/payload/src/fields/baseFields/baseIDField.ts +++ b/packages/payload/src/fields/baseFields/baseIDField.ts @@ -1,9 +1,9 @@ -import ObjectID from 'bson-objectid' +import { ObjectId } from 'bson' import type { Field, FieldHook } from '../config/types' const generateID: FieldHook = ({ operation, value }) => - (operation !== 'create' ? value : false) || new ObjectID().toHexString() + (operation !== 'create' ? value : false) || new ObjectId().toHexString() export const baseIDField: Field = { name: 'id', diff --git a/packages/payload/src/preferences/requestHandlers/update.ts b/packages/payload/src/preferences/requestHandlers/update.ts index 23a3823cd4..845f172306 100644 --- a/packages/payload/src/preferences/requestHandlers/update.ts +++ b/packages/payload/src/preferences/requestHandlers/update.ts @@ -16,7 +16,7 @@ export const updateHandler: PayloadHandler = async ({ req, routeParams }) => { return Response.json( { - ...doc, + doc, message: payloadRequest.t('general:updatedSuccessfully'), }, { diff --git a/packages/payload/src/utilities/isValidID.ts b/packages/payload/src/utilities/isValidID.ts index d5b5b467f7..0875c4de89 100644 --- a/packages/payload/src/utilities/isValidID.ts +++ b/packages/payload/src/utilities/isValidID.ts @@ -1,4 +1,4 @@ -import ObjectID from 'bson-objectid' +import { ObjectId } from 'bson' export const isValidID = ( value: number | string, @@ -8,6 +8,6 @@ export const isValidID = ( if (typeof value === 'number' && !Number.isNaN(value)) return true if (type === 'ObjectID') { - return ObjectID.isValid(String(value)) + return ObjectId.isValid(String(value)) } } diff --git a/packages/ui/package.json b/packages/ui/package.json index 15e00d82f8..fb710f8022 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -45,7 +45,7 @@ "@monaco-editor/react": "4.5.1", "@payloadcms/translations": "workspace:^", "body-scroll-lock": "4.0.0-beta.0", - "bson-objectid": "2.0.4", + "bson": "^6.3.0", "date-fns": "2.30.0", "deep-equal": "2.2.2", "flatley": "5.2.0", diff --git a/packages/ui/src/forms/Form/fieldReducer.ts b/packages/ui/src/forms/Form/fieldReducer.ts index e2d44bcfd8..b9c0b4c012 100644 --- a/packages/ui/src/forms/Form/fieldReducer.ts +++ b/packages/ui/src/forms/Form/fieldReducer.ts @@ -1,4 +1,4 @@ -import ObjectID from 'bson-objectid' +import { ObjectId } from 'bson' import equal from 'deep-equal' import type { FieldAction, FormState, FormField, Row } from './types' @@ -105,7 +105,7 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState { const withNewRow = [...(state[path]?.rows || [])] const newRow: Row = { - id: new ObjectID().toHexString(), + id: new ObjectId().toHexString(), blockType: blockType || undefined, collapsed: false, } @@ -147,7 +147,7 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState { const rowsMetadata = [...(state[path]?.rows || [])] rowsMetadata[rowIndex] = { - id: new ObjectID().toHexString(), + id: new ObjectId().toHexString(), blockType: blockType || undefined, collapsed: false, } @@ -183,10 +183,10 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState { const rowsMetadata = state[path]?.rows || [] const duplicateRowMetadata = deepCopyObject(rowsMetadata[rowIndex]) - if (duplicateRowMetadata.id) duplicateRowMetadata.id = new ObjectID().toHexString() + if (duplicateRowMetadata.id) duplicateRowMetadata.id = new ObjectId().toHexString() const duplicateRowState = deepCopyObject(rows[rowIndex]) - if (duplicateRowState.id) duplicateRowState.id = new ObjectID().toHexString() + if (duplicateRowState.id) duplicateRowState.id = new ObjectId().toHexString() // If there are subfields if (Object.keys(duplicateRowState).length > 0) { diff --git a/packages/ui/src/forms/utilities/buildStateFromSchema/addFieldStatePromise.ts b/packages/ui/src/forms/utilities/buildStateFromSchema/addFieldStatePromise.ts index 93a1c170ca..22e1e7738f 100644 --- a/packages/ui/src/forms/utilities/buildStateFromSchema/addFieldStatePromise.ts +++ b/packages/ui/src/forms/utilities/buildStateFromSchema/addFieldStatePromise.ts @@ -1,7 +1,7 @@ /* eslint-disable no-param-reassign */ import type { TFunction } from '@payloadcms/translations' -import ObjectID from 'bson-objectid' +import { ObjectId } from 'bson' import type { User } from 'payload/auth' import type { NonPresentationalField, Data, SanitizedConfig } from 'payload/types' @@ -96,7 +96,7 @@ export const addFieldStatePromise = async ({ const { promises, rowMetadata } = arrayValue.reduce( (acc, row, i) => { const rowPath = `${path}${field.name}.${i}.` - row.id = row?.id || new ObjectID().toHexString() + row.id = row?.id || new ObjectId().toHexString() state[`${rowPath}id`] = { initialValue: row.id, @@ -173,7 +173,7 @@ export const addFieldStatePromise = async ({ const rowPath = `${path}${field.name}.${i}.` if (block) { - row.id = row?.id || new ObjectID().toHexString() + row.id = row?.id || new ObjectId().toHexString() state[`${rowPath}id`] = { initialValue: row.id, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71410fcc3e..1aca014816 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -301,6 +301,9 @@ importers: packages/db-mongodb: dependencies: + bson: + specifier: ^6.3.0 + version: 6.3.0 bson-ext: specifier: ^4.0.3 version: 4.0.3 @@ -595,9 +598,9 @@ importers: '@payloadcms/translations': specifier: workspace:^ version: link:../translations - bson-objectid: - specifier: 2.0.4 - version: 2.0.4 + bson: + specifier: ^6.3.0 + version: 6.3.0 conf: specifier: 10.2.0 version: 10.2.0 @@ -1273,9 +1276,9 @@ importers: body-scroll-lock: specifier: 4.0.0-beta.0 version: 4.0.0-beta.0 - bson-objectid: - specifier: 2.0.4 - version: 2.0.4 + bson: + specifier: ^6.3.0 + version: 6.3.0 date-fns: specifier: 2.30.0 version: 2.30.0 @@ -7108,6 +7111,11 @@ packages: engines: {node: '>=14.20.1'} dev: false + /bson@6.3.0: + resolution: {integrity: sha512-balJfqwwTBddxfnidJZagCBPP/f48zj9Sdp3OJswREOgsJzHiQSaOIAtApSgDQFYgHqAvFkp53AFSqjMDZoTFw==} + engines: {node: '>=16.20.1'} + dev: false + /buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} dev: true diff --git a/test/auth/int.spec.ts b/test/auth/int.spec.ts index 03f658a1e8..d929e21748 100644 --- a/test/auth/int.spec.ts +++ b/test/auth/int.spec.ts @@ -10,7 +10,6 @@ import { startMemoryDB } from '../startMemoryDB' import configPromise from './config' import { namedSaveToJWTValue, saveToJWTKey, slug } from './shared' -let apiUrl let restClient: NextRESTClient let payload: Payload @@ -333,7 +332,7 @@ describe('Auth', () => { let data beforeAll(async () => { - const response = await restClient.POST(`/${slug}/payload-preferences/${key}`, { + const response = await restClient.POST(`/payload-preferences/${key}`, { body: JSON.stringify({ value: { property }, }), @@ -350,7 +349,7 @@ describe('Auth', () => { }) it('should read', async () => { - const response = await restClient.GET(`/${slug}/payload-preferences/${key}`, { + const response = await restClient.GET(`/payload-preferences/${key}`, { headers: { Authorization: `JWT ${token}`, }, @@ -361,7 +360,7 @@ describe('Auth', () => { }) it('should update', async () => { - const response = await restClient.POST(`/${slug}/payload-preferences/${key}`, { + const response = await restClient.POST(`/payload-preferences/${key}`, { body: JSON.stringify({ value: { property: 'updated', property2: 'test' }, }), @@ -388,7 +387,7 @@ describe('Auth', () => { }) it('should delete', async () => { - const response = await restClient.DELETE(`/${slug}/payload-preferences/${key}`, { + const response = await restClient.DELETE(`/payload-preferences/${key}`, { headers: { Authorization: `JWT ${token}`, }, @@ -606,14 +605,6 @@ describe('Auth', () => { }) }) - describe('REST API', () => { - it('should respond from route handlers', async () => { - const test = await fetch(`${apiUrl}/api/test`) - - expect(test.status).toStrictEqual(200) - }) - }) - describe('API Key', () => { it('should authenticate via the correct API key user', async () => { const usersQuery = await payload.find({ From 4bb10240417da17a16d4362f44f44cec0e1b3d9d Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Thu, 15 Feb 2024 15:14:29 -0500 Subject: [PATCH 2/5] chore(ui): threads params through context and conditionally renders document tabs (#5094) --- packages/payload/src/admin/elements/Tab.ts | 2 - .../DocumentHeader/Tabs/ShouldRenderTabs.tsx | 20 ++++++++++ .../elements/DocumentHeader/Tabs/index.tsx | 14 +++---- .../ui/src/elements/DocumentHeader/index.tsx | 20 +--------- .../ui/src/elements/ListControls/index.tsx | 4 +- packages/ui/src/elements/Localizer/index.tsx | 2 +- packages/ui/src/elements/Pagination/index.tsx | 4 +- packages/ui/src/elements/PerPage/index.tsx | 6 +-- .../ui/src/elements/RenderTitle/index.tsx | 10 +++-- packages/ui/src/elements/RenderTitle/types.ts | 3 -- .../ui/src/elements/SearchFilter/index.tsx | 10 +++-- packages/ui/src/elements/SortColumn/index.tsx | 8 ++-- .../ui/src/elements/SortComplex/index.tsx | 8 ++-- .../ui/src/elements/WhereBuilder/index.tsx | 38 ++++++++++--------- packages/ui/src/providers/Locale/index.tsx | 2 +- packages/ui/src/providers/Root/index.tsx | 29 +++++++------- .../ui/src/providers/SearchParams/index.tsx | 31 ++++++++++----- packages/ui/src/views/Edit/action.ts | 6 ++- 18 files changed, 119 insertions(+), 98 deletions(-) create mode 100644 packages/ui/src/elements/DocumentHeader/Tabs/ShouldRenderTabs.tsx diff --git a/packages/payload/src/admin/elements/Tab.ts b/packages/payload/src/admin/elements/Tab.ts index f046b9aba5..2f5435e7d1 100644 --- a/packages/payload/src/admin/elements/Tab.ts +++ b/packages/payload/src/admin/elements/Tab.ts @@ -12,8 +12,6 @@ export type DocumentTabProps = { config: SanitizedConfig globalConfig?: SanitizedGlobalConfig i18n: I18n - id: string - isEditing?: boolean } export type DocumentTabCondition = (args: { diff --git a/packages/ui/src/elements/DocumentHeader/Tabs/ShouldRenderTabs.tsx b/packages/ui/src/elements/DocumentHeader/Tabs/ShouldRenderTabs.tsx new file mode 100644 index 0000000000..36b9e5db75 --- /dev/null +++ b/packages/ui/src/elements/DocumentHeader/Tabs/ShouldRenderTabs.tsx @@ -0,0 +1,20 @@ +'use client' +import React from 'react' +import { useSearchParams } from '../../../providers/SearchParams' + +export const ShouldRenderTabs: React.FC<{ + children: React.ReactNode +}> = ({ children }) => { + const { + params: { collection: collectionSlug, global: globalSlug, segments: [idFromParam] = [] }, + } = useSearchParams() + + const id = idFromParam !== 'create' ? idFromParam : null + + // Don't show tabs when creating new documents + if ((collectionSlug && id) || globalSlug) { + return children + } + + return null +} diff --git a/packages/ui/src/elements/DocumentHeader/Tabs/index.tsx b/packages/ui/src/elements/DocumentHeader/Tabs/index.tsx index c4393a1233..e2b80b6330 100644 --- a/packages/ui/src/elements/DocumentHeader/Tabs/index.tsx +++ b/packages/ui/src/elements/DocumentHeader/Tabs/index.tsx @@ -5,19 +5,19 @@ import { getCustomViews } from './getCustomViews' import { getViewConfig } from './getViewConfig' import { tabs as defaultViews } from './tabs' import { DocumentTabProps } from 'payload/types' +import { ShouldRenderTabs } from './ShouldRenderTabs' import './index.scss' const baseClass = 'doc-tabs' export const DocumentTabs: React.FC = (props) => { - const { collectionConfig, globalConfig, isEditing } = props + const { collectionConfig, globalConfig } = props const customViews = getCustomViews({ collectionConfig, globalConfig }) - // Don't show tabs when creating new documents - if ((collectionConfig && isEditing) || global) { - return ( + return ( +
    @@ -69,8 +69,6 @@ export const DocumentTabs: React.FC = (props) => {
- ) - } - - return null +
+ ) } diff --git a/packages/ui/src/elements/DocumentHeader/index.tsx b/packages/ui/src/elements/DocumentHeader/index.tsx index c88b9332ce..ea0c48f1f7 100644 --- a/packages/ui/src/elements/DocumentHeader/index.tsx +++ b/packages/ui/src/elements/DocumentHeader/index.tsx @@ -16,27 +16,13 @@ import './index.scss' const baseClass = `doc-header` export const DocumentHeader: React.FC<{ - apiURL?: string config: SanitizedConfig collectionConfig?: SanitizedCollectionConfig customHeader?: React.ReactNode - data?: any globalConfig?: SanitizedGlobalConfig - id?: string - isEditing?: boolean i18n: I18n }> = (props) => { - const { - id, - apiURL, - config, - collectionConfig, - customHeader, - data, - globalConfig, - isEditing, - i18n, - } = props + const { config, collectionConfig, customHeader, globalConfig, i18n } = props const titleFieldConfig = collectionConfig?.fields?.find( (f) => 'name' in f && f?.name === collectionConfig?.admin?.useAsTitle, @@ -52,7 +38,6 @@ export const DocumentHeader: React.FC<{ useAsTitle={collectionConfig?.admin?.useAsTitle} globalLabel={globalConfig?.label} globalSlug={globalConfig?.slug} - data={data} isDate={titleFieldConfig?.type === 'date'} dateFormat={ titleFieldConfig && 'date' in titleFieldConfig?.admin @@ -62,12 +47,9 @@ export const DocumentHeader: React.FC<{ fallback={`[${i18n.t('general:untitled')}]`} /> diff --git a/packages/ui/src/elements/ListControls/index.tsx b/packages/ui/src/elements/ListControls/index.tsx index 03977d9afb..c312b30dae 100644 --- a/packages/ui/src/elements/ListControls/index.tsx +++ b/packages/ui/src/elements/ListControls/index.tsx @@ -46,8 +46,8 @@ export const ListControls: React.FC = (props) => { textFieldsToBeSearched, } = props - const params = useSearchParams() - const shouldInitializeWhereOpened = validateWhereQuery(params?.where) + const { searchParams } = useSearchParams() + const shouldInitializeWhereOpened = validateWhereQuery(searchParams?.where) const [visibleDrawer, setVisibleDrawer] = useState<'columns' | 'sort' | 'where'>( shouldInitializeWhereOpened ? 'where' : undefined, diff --git a/packages/ui/src/elements/Localizer/index.tsx b/packages/ui/src/elements/Localizer/index.tsx index 96e13c7ce9..a2bb1fec11 100644 --- a/packages/ui/src/elements/Localizer/index.tsx +++ b/packages/ui/src/elements/Localizer/index.tsx @@ -22,7 +22,7 @@ const Localizer: React.FC<{ const { i18n } = useTranslation() const locale = useLocale() - const searchParams = useSearchParams() + const { searchParams } = useSearchParams() const localeLabel = getTranslation(locale.label, i18n) diff --git a/packages/ui/src/elements/Pagination/index.tsx b/packages/ui/src/elements/Pagination/index.tsx index e0e639c35a..b03901f523 100644 --- a/packages/ui/src/elements/Pagination/index.tsx +++ b/packages/ui/src/elements/Pagination/index.tsx @@ -21,7 +21,7 @@ const baseClass = 'paginator' export const Pagination: React.FC = (props) => { const router = useRouter() - const params = useSearchParams() + const { searchParams } = useSearchParams() const pathname = usePathname() const { @@ -41,7 +41,7 @@ export const Pagination: React.FC = (props) => { const updatePage = (page) => { if (!disableHistoryChange) { const newParams = { - ...params, + ...searchParams, } newParams.page = page diff --git a/packages/ui/src/elements/PerPage/index.tsx b/packages/ui/src/elements/PerPage/index.tsx index 38623eeccc..afe6a12694 100644 --- a/packages/ui/src/elements/PerPage/index.tsx +++ b/packages/ui/src/elements/PerPage/index.tsx @@ -30,7 +30,7 @@ export const PerPage: React.FC = ({ modifySearchParams = true, resetPage = false, }) => { - const params = useSearchParams() + const { searchParams } = useSearchParams() const history = useHistory() const { t } = useTranslation() @@ -63,9 +63,9 @@ export const PerPage: React.FC = ({ history.replace({ search: qs.stringify( { - ...params, + ...searchParams, limit: limitNumber, - page: resetPage ? 1 : params.page, + page: resetPage ? 1 : searchParams.page, }, { addQueryPrefix: true }, ), diff --git a/packages/ui/src/elements/RenderTitle/index.tsx b/packages/ui/src/elements/RenderTitle/index.tsx index fe3e6da7a7..4fbec82409 100644 --- a/packages/ui/src/elements/RenderTitle/index.tsx +++ b/packages/ui/src/elements/RenderTitle/index.tsx @@ -5,6 +5,7 @@ import type { Props } from './types' import useTitle from '../../hooks/useTitle' import IDLabel from '../IDLabel' +import { useDocumentInfo } from '../../providers/DocumentInfo' import './index.scss' const baseClass = 'render-title' @@ -15,12 +16,13 @@ const RenderTitle: React.FC = (props) => { useAsTitle, globalLabel, globalSlug, - data, element = 'h1', fallback = '[untitled]', title: titleFromProps, } = props + const { id } = useDocumentInfo() + const titleFromForm = useTitle({ useAsTitle: useAsTitle, globalLabel: globalLabel, @@ -28,11 +30,11 @@ const RenderTitle: React.FC = (props) => { }) let title = titleFromForm - if (!title) title = data?.id + if (!title) title = id?.toString() if (!title) title = fallback title = titleFromProps || title - const idAsTitle = title === data?.id + const idAsTitle = title === id const Tag = element @@ -43,7 +45,7 @@ const RenderTitle: React.FC = (props) => { .join(' ')} title={title} > - {idAsTitle ? : title} + {idAsTitle ? : title} ) } diff --git a/packages/ui/src/elements/RenderTitle/types.ts b/packages/ui/src/elements/RenderTitle/types.ts index 1f88cc742b..a65fb93caa 100644 --- a/packages/ui/src/elements/RenderTitle/types.ts +++ b/packages/ui/src/elements/RenderTitle/types.ts @@ -5,9 +5,6 @@ export type Props = { useAsTitle?: SanitizedCollectionConfig['admin']['useAsTitle'] globalLabel?: SanitizedGlobalConfig['label'] globalSlug?: SanitizedGlobalConfig['slug'] - data?: { - id?: string - } element?: React.ElementType fallback?: string title?: string diff --git a/packages/ui/src/elements/SearchFilter/index.tsx b/packages/ui/src/elements/SearchFilter/index.tsx index 996a0a190e..d353ec0872 100644 --- a/packages/ui/src/elements/SearchFilter/index.tsx +++ b/packages/ui/src/elements/SearchFilter/index.tsx @@ -22,11 +22,13 @@ const SearchFilter: React.FC = (props) => { modifySearchQuery = true, } = props - const params = useSearchParams() + const { searchParams } = useSearchParams() const history = useHistory() const { i18n, t } = useTranslation() - const [search, setSearch] = useState(typeof params?.search === 'string' ? params?.search : '') + const [search, setSearch] = useState( + typeof searchParams?.search === 'string' ? searchParams?.search : '', + ) const [previousSearch, setPreviousSearch] = useState('') const placeholder = useRef(t('general:searchBy', { label: getTranslation(fieldLabel, i18n) })) @@ -40,7 +42,7 @@ const SearchFilter: React.FC = (props) => { if (modifySearchQuery) { history.replace({ search: queryString.stringify({ - ...params, + ...searchParams, page: 1, search: debouncedSearch || undefined, }), @@ -54,7 +56,7 @@ const SearchFilter: React.FC = (props) => { previousSearch, history, fieldName, - params, + searchParams, handleChange, modifySearchQuery, listSearchableFields, diff --git a/packages/ui/src/elements/SortColumn/index.tsx b/packages/ui/src/elements/SortColumn/index.tsx index a1099d2b92..3b64a0a68a 100644 --- a/packages/ui/src/elements/SortColumn/index.tsx +++ b/packages/ui/src/elements/SortColumn/index.tsx @@ -15,11 +15,11 @@ const baseClass = 'sort-column' export const SortColumn: React.FC = (props) => { const { name, disable = false, label } = props - const params = useSearchParams() + const { searchParams } = useSearchParams() const history = useHistory() const { i18n, t } = useTranslation() - const { sort } = params + const { sort } = searchParams const desc = `-${name}` const asc = name @@ -35,14 +35,14 @@ export const SortColumn: React.FC = (props) => { history.push({ search: queryString.stringify( { - ...params, + ...searchParams, sort: newSort, }, { addQueryPrefix: true }, ), }) }, - [params, history], + [searchParams, history], ) return ( diff --git a/packages/ui/src/elements/SortComplex/index.tsx b/packages/ui/src/elements/SortComplex/index.tsx index 90bf4134eb..840af0db93 100644 --- a/packages/ui/src/elements/SortComplex/index.tsx +++ b/packages/ui/src/elements/SortComplex/index.tsx @@ -19,7 +19,7 @@ const SortComplex: React.FC = (props) => { const { collection, handleChange, modifySearchQuery = true } = props const history = useHistory() - const params = useSearchParams() + const { searchParams } = useSearchParams() const { i18n, t } = useTranslation() const [sortOptions, setSortOptions] = useState() @@ -45,11 +45,11 @@ const SortComplex: React.FC = (props) => { if (handleChange) handleChange(newSortValue) - if (params.sort !== newSortValue && modifySearchQuery) { + if (searchParams.sort !== newSortValue && modifySearchQuery) { history.replace({ search: queryString.stringify( { - ...params, + ...searchParams, sort: newSortValue, }, { addQueryPrefix: true }, @@ -57,7 +57,7 @@ const SortComplex: React.FC = (props) => { }) } } - }, [history, params, sortField, sortOrder, modifySearchQuery, handleChange]) + }, [history, searchParams, sortField, sortOrder, modifySearchQuery, handleChange]) useEffect(() => { setSortOptions([ diff --git a/packages/ui/src/elements/WhereBuilder/index.tsx b/packages/ui/src/elements/WhereBuilder/index.tsx index b926adbdb7..e126ffd3d1 100644 --- a/packages/ui/src/elements/WhereBuilder/index.tsx +++ b/packages/ui/src/elements/WhereBuilder/index.tsx @@ -66,28 +66,32 @@ const WhereBuilder: React.FC = (props) => { const collection = config.collections.find((c) => c.slug === collectionSlug) const [reducedFields] = useState(() => reduceFields(collection.fields, i18n)) - const params = useSearchParams() + const { searchParams } = useSearchParams() // This handles initializing the where conditions from the search query (URL). That way, if you pass in // query params to the URL, the where conditions will be initialized from those and displayed in the UI. // Example: /admin/collections/posts?where[or][0][and][0][text][equals]=example%20post - const [conditions, dispatchConditions] = useReducer(reducer, params.where, (whereFromSearch) => { - if (modifySearchQuery && whereFromSearch) { - if (validateWhereQuery(whereFromSearch)) { - return whereFromSearch.or + const [conditions, dispatchConditions] = useReducer( + reducer, + searchParams.where, + (whereFromSearch) => { + if (modifySearchQuery && whereFromSearch) { + if (validateWhereQuery(whereFromSearch)) { + return whereFromSearch.or + } + + // Transform the where query to be in the right format. This will transform something simple like [text][equals]=example%20post to the right format + const transformedWhere = transformWhereQuery(whereFromSearch) + + if (validateWhereQuery(transformedWhere)) { + return transformedWhere.or + } + + console.warn('Invalid where query in URL. Ignoring.') } - - // Transform the where query to be in the right format. This will transform something simple like [text][equals]=example%20post to the right format - const transformedWhere = transformWhereQuery(whereFromSearch) - - if (validateWhereQuery(transformedWhere)) { - return transformedWhere.or - } - - console.warn('Invalid where query in URL. Ignoring.') - } - return [] - }) + return [] + }, + ) // This handles updating the search query (URL) when the where conditions change // useThrottledEffect( diff --git a/packages/ui/src/providers/Locale/index.tsx b/packages/ui/src/providers/Locale/index.tsx index 7d0cf060af..a28b0edeb7 100644 --- a/packages/ui/src/providers/Locale/index.tsx +++ b/packages/ui/src/providers/Locale/index.tsx @@ -20,7 +20,7 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child const defaultLocale = localization && localization.defaultLocale ? localization.defaultLocale : 'en' - const searchParams = useSearchParams() + const { searchParams } = useSearchParams() const [localeCode, setLocaleCode] = useState( (searchParams?.locale as string) || defaultLocale, diff --git a/packages/ui/src/providers/Root/index.tsx b/packages/ui/src/providers/Root/index.tsx index 11d88ce46b..c9b2a3d0b8 100644 --- a/packages/ui/src/providers/Root/index.tsx +++ b/packages/ui/src/providers/Root/index.tsx @@ -20,6 +20,7 @@ import { DocumentEventsProvider } from '../DocumentEvents' import { CustomProvider } from '../CustomProvider' import { ComponentMap } from '../../utilities/buildComponentMap/types' import { ComponentMapProvider } from '../ComponentMapProvider' +import { SearchParamsProvider } from '../SearchParams' type Props = { config: ClientConfig @@ -63,19 +64,21 @@ export const RootProvider: React.FC = ({ - - - - - - - {children} - - - - - - + + + + + + + + {children} + + + + + + + diff --git a/packages/ui/src/providers/SearchParams/index.tsx b/packages/ui/src/providers/SearchParams/index.tsx index 08cbad715c..ee3e56d9ca 100644 --- a/packages/ui/src/providers/SearchParams/index.tsx +++ b/packages/ui/src/providers/SearchParams/index.tsx @@ -1,16 +1,27 @@ 'use client' -import { useSearchParams as useNextSearchParams } from 'next/navigation' +import { Params } from 'next/dist/shared/lib/router/utils/route-matcher' +import { useSearchParams as useNextSearchParams, useParams as useNextParams } from 'next/navigation' import qs from 'qs' import React, { createContext, useContext } from 'react' -const Context = createContext({}) - -export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => { - const searchParams = useNextSearchParams() - - const params = qs.parse(searchParams.toString(), { depth: 10, ignoreQueryPrefix: true }) - - return {children} +interface IParamsContext { + searchParams: qs.ParsedQs + params: Params } -export const useSearchParams = (): qs.ParsedQs => useContext(Context) +const Context = createContext({ + searchParams: {}, + params: {}, +} as IParamsContext) + +// TODO: abstract the `next/navigation` dependency out from this provider so that it can be used in other contexts +export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => { + const nextSearchParams = useNextSearchParams() + const params = useNextParams() + + const searchParams = qs.parse(nextSearchParams.toString(), { depth: 10, ignoreQueryPrefix: true }) + + return {children} +} + +export const useSearchParams = (): IParamsContext => useContext(Context) diff --git a/packages/ui/src/views/Edit/action.ts b/packages/ui/src/views/Edit/action.ts index 7d21bf6852..3687677bcf 100644 --- a/packages/ui/src/views/Edit/action.ts +++ b/packages/ui/src/views/Edit/action.ts @@ -32,7 +32,11 @@ export const getFormStateFromServer = async ( config: configPromise, }) - const collectionConfig = payload.collections[collectionSlug].config + const collectionConfig = payload.collections[collectionSlug]?.config + + if (!collectionConfig) { + throw new Error(`Collection with slug "${collectionSlug}" not found`) + } const data = reduceFieldsToValues(formState, true) From ac754f86f395b6b65242d1a23920c9eb955b38fd Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Thu, 15 Feb 2024 15:52:04 -0500 Subject: [PATCH 3/5] feat(ui): adds params context (#5095) --- .../DocumentHeader/Tabs/ShouldRenderTabs.tsx | 8 +++-- .../ui/src/elements/ListControls/index.tsx | 2 +- packages/ui/src/elements/Localizer/index.tsx | 2 +- packages/ui/src/elements/Pagination/index.tsx | 2 +- packages/ui/src/elements/PerPage/index.tsx | 2 +- .../ui/src/elements/SearchFilter/index.tsx | 2 +- packages/ui/src/elements/SortColumn/index.tsx | 2 +- .../ui/src/elements/SortComplex/index.tsx | 2 +- .../ui/src/elements/WhereBuilder/index.tsx | 2 +- packages/ui/src/providers/Locale/index.tsx | 2 +- packages/ui/src/providers/Params/index.tsx | 16 +++++++++ packages/ui/src/providers/Root/index.tsx | 33 ++++++++++--------- .../ui/src/providers/SearchParams/index.tsx | 20 +++-------- 13 files changed, 53 insertions(+), 42 deletions(-) create mode 100644 packages/ui/src/providers/Params/index.tsx diff --git a/packages/ui/src/elements/DocumentHeader/Tabs/ShouldRenderTabs.tsx b/packages/ui/src/elements/DocumentHeader/Tabs/ShouldRenderTabs.tsx index 36b9e5db75..92dd8a10a0 100644 --- a/packages/ui/src/elements/DocumentHeader/Tabs/ShouldRenderTabs.tsx +++ b/packages/ui/src/elements/DocumentHeader/Tabs/ShouldRenderTabs.tsx @@ -1,13 +1,15 @@ 'use client' import React from 'react' -import { useSearchParams } from '../../../providers/SearchParams' +import { useParams } from '../../../providers/Params' export const ShouldRenderTabs: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { - params: { collection: collectionSlug, global: globalSlug, segments: [idFromParam] = [] }, - } = useSearchParams() + collection: collectionSlug, + global: globalSlug, + segments: [idFromParam] = [], + } = useParams() const id = idFromParam !== 'create' ? idFromParam : null diff --git a/packages/ui/src/elements/ListControls/index.tsx b/packages/ui/src/elements/ListControls/index.tsx index c312b30dae..89ca6866d6 100644 --- a/packages/ui/src/elements/ListControls/index.tsx +++ b/packages/ui/src/elements/ListControls/index.tsx @@ -46,7 +46,7 @@ export const ListControls: React.FC = (props) => { textFieldsToBeSearched, } = props - const { searchParams } = useSearchParams() + const searchParams = useSearchParams() const shouldInitializeWhereOpened = validateWhereQuery(searchParams?.where) const [visibleDrawer, setVisibleDrawer] = useState<'columns' | 'sort' | 'where'>( diff --git a/packages/ui/src/elements/Localizer/index.tsx b/packages/ui/src/elements/Localizer/index.tsx index a2bb1fec11..96e13c7ce9 100644 --- a/packages/ui/src/elements/Localizer/index.tsx +++ b/packages/ui/src/elements/Localizer/index.tsx @@ -22,7 +22,7 @@ const Localizer: React.FC<{ const { i18n } = useTranslation() const locale = useLocale() - const { searchParams } = useSearchParams() + const searchParams = useSearchParams() const localeLabel = getTranslation(locale.label, i18n) diff --git a/packages/ui/src/elements/Pagination/index.tsx b/packages/ui/src/elements/Pagination/index.tsx index b03901f523..71acbe81e1 100644 --- a/packages/ui/src/elements/Pagination/index.tsx +++ b/packages/ui/src/elements/Pagination/index.tsx @@ -21,7 +21,7 @@ const baseClass = 'paginator' export const Pagination: React.FC = (props) => { const router = useRouter() - const { searchParams } = useSearchParams() + const searchParams = useSearchParams() const pathname = usePathname() const { diff --git a/packages/ui/src/elements/PerPage/index.tsx b/packages/ui/src/elements/PerPage/index.tsx index afe6a12694..eed4f1c64d 100644 --- a/packages/ui/src/elements/PerPage/index.tsx +++ b/packages/ui/src/elements/PerPage/index.tsx @@ -30,7 +30,7 @@ export const PerPage: React.FC = ({ modifySearchParams = true, resetPage = false, }) => { - const { searchParams } = useSearchParams() + const searchParams = useSearchParams() const history = useHistory() const { t } = useTranslation() diff --git a/packages/ui/src/elements/SearchFilter/index.tsx b/packages/ui/src/elements/SearchFilter/index.tsx index d353ec0872..fb5b0c8404 100644 --- a/packages/ui/src/elements/SearchFilter/index.tsx +++ b/packages/ui/src/elements/SearchFilter/index.tsx @@ -22,7 +22,7 @@ const SearchFilter: React.FC = (props) => { modifySearchQuery = true, } = props - const { searchParams } = useSearchParams() + const searchParams = useSearchParams() const history = useHistory() const { i18n, t } = useTranslation() diff --git a/packages/ui/src/elements/SortColumn/index.tsx b/packages/ui/src/elements/SortColumn/index.tsx index 3b64a0a68a..99ee97a3cb 100644 --- a/packages/ui/src/elements/SortColumn/index.tsx +++ b/packages/ui/src/elements/SortColumn/index.tsx @@ -15,7 +15,7 @@ const baseClass = 'sort-column' export const SortColumn: React.FC = (props) => { const { name, disable = false, label } = props - const { searchParams } = useSearchParams() + const searchParams = useSearchParams() const history = useHistory() const { i18n, t } = useTranslation() diff --git a/packages/ui/src/elements/SortComplex/index.tsx b/packages/ui/src/elements/SortComplex/index.tsx index 840af0db93..95c8380438 100644 --- a/packages/ui/src/elements/SortComplex/index.tsx +++ b/packages/ui/src/elements/SortComplex/index.tsx @@ -19,7 +19,7 @@ const SortComplex: React.FC = (props) => { const { collection, handleChange, modifySearchQuery = true } = props const history = useHistory() - const { searchParams } = useSearchParams() + const searchParams = useSearchParams() const { i18n, t } = useTranslation() const [sortOptions, setSortOptions] = useState() diff --git a/packages/ui/src/elements/WhereBuilder/index.tsx b/packages/ui/src/elements/WhereBuilder/index.tsx index e126ffd3d1..6bb4de5b47 100644 --- a/packages/ui/src/elements/WhereBuilder/index.tsx +++ b/packages/ui/src/elements/WhereBuilder/index.tsx @@ -66,7 +66,7 @@ const WhereBuilder: React.FC = (props) => { const collection = config.collections.find((c) => c.slug === collectionSlug) const [reducedFields] = useState(() => reduceFields(collection.fields, i18n)) - const { searchParams } = useSearchParams() + const searchParams = useSearchParams() // This handles initializing the where conditions from the search query (URL). That way, if you pass in // query params to the URL, the where conditions will be initialized from those and displayed in the UI. diff --git a/packages/ui/src/providers/Locale/index.tsx b/packages/ui/src/providers/Locale/index.tsx index a28b0edeb7..7d0cf060af 100644 --- a/packages/ui/src/providers/Locale/index.tsx +++ b/packages/ui/src/providers/Locale/index.tsx @@ -20,7 +20,7 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child const defaultLocale = localization && localization.defaultLocale ? localization.defaultLocale : 'en' - const { searchParams } = useSearchParams() + const searchParams = useSearchParams() const [localeCode, setLocaleCode] = useState( (searchParams?.locale as string) || defaultLocale, diff --git a/packages/ui/src/providers/Params/index.tsx b/packages/ui/src/providers/Params/index.tsx new file mode 100644 index 0000000000..1d1084192b --- /dev/null +++ b/packages/ui/src/providers/Params/index.tsx @@ -0,0 +1,16 @@ +'use client' +import { Params } from 'next/dist/shared/lib/router/utils/route-matcher' +import { useParams as useNextParams } from 'next/navigation' +import React, { createContext, useContext } from 'react' + +interface IParamsContext extends Params {} + +const Context = createContext({} as IParamsContext) + +// TODO: abstract the `next/navigation` dependency out from this provider so that it can be used in other contexts +export const ParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => { + const params = useNextParams() + return {children} +} + +export const useParams = (): IParamsContext => useContext(Context) diff --git a/packages/ui/src/providers/Root/index.tsx b/packages/ui/src/providers/Root/index.tsx index c9b2a3d0b8..e8b8263d31 100644 --- a/packages/ui/src/providers/Root/index.tsx +++ b/packages/ui/src/providers/Root/index.tsx @@ -21,6 +21,7 @@ import { CustomProvider } from '../CustomProvider' import { ComponentMap } from '../../utilities/buildComponentMap/types' import { ComponentMapProvider } from '../ComponentMapProvider' import { SearchParamsProvider } from '../SearchParams' +import { ParamsProvider } from '../Params' type Props = { config: ClientConfig @@ -64,21 +65,23 @@ export const RootProvider: React.FC = ({ - - - - - - - - {children} - - - - - - - + + + + + + + + + {children} + + + + + + + + diff --git a/packages/ui/src/providers/SearchParams/index.tsx b/packages/ui/src/providers/SearchParams/index.tsx index ee3e56d9ca..e3a137e344 100644 --- a/packages/ui/src/providers/SearchParams/index.tsx +++ b/packages/ui/src/providers/SearchParams/index.tsx @@ -1,27 +1,17 @@ 'use client' -import { Params } from 'next/dist/shared/lib/router/utils/route-matcher' -import { useSearchParams as useNextSearchParams, useParams as useNextParams } from 'next/navigation' +import { useSearchParams as useNextSearchParams } from 'next/navigation' import qs from 'qs' import React, { createContext, useContext } from 'react' -interface IParamsContext { - searchParams: qs.ParsedQs - params: Params -} +interface ISearchParamsContext extends qs.ParsedQs {} -const Context = createContext({ - searchParams: {}, - params: {}, -} as IParamsContext) +const Context = createContext({} as ISearchParamsContext) // TODO: abstract the `next/navigation` dependency out from this provider so that it can be used in other contexts export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => { const nextSearchParams = useNextSearchParams() - const params = useNextParams() - const searchParams = qs.parse(nextSearchParams.toString(), { depth: 10, ignoreQueryPrefix: true }) - - return {children} + return {children} } -export const useSearchParams = (): IParamsContext => useContext(Context) +export const useSearchParams = (): ISearchParamsContext => useContext(Context) From 88457d726baee40f360afb36d35bf1060e191522 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 15 Feb 2024 21:44:37 -0500 Subject: [PATCH 4/5] chore: removes unecessary memory allocation for urlPropertiesObject object --- .../src/utilities/createPayloadRequest.ts | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/packages/next/src/utilities/createPayloadRequest.ts b/packages/next/src/utilities/createPayloadRequest.ts index 663744d0dc..d2cb3306c9 100644 --- a/packages/next/src/utilities/createPayloadRequest.ts +++ b/packages/next/src/utilities/createPayloadRequest.ts @@ -36,21 +36,7 @@ export const createPayloadRequest = async ({ } const urlProperties = new URL(request.url) - - // NOTE: URL properties are not enumerable, so we need to convert them to an object - const urlPropertiesObject = { - searchParams: urlProperties.searchParams, - pathname: urlProperties.pathname, - port: urlProperties.port, - protocol: urlProperties.protocol, - search: urlProperties.search, - origin: urlProperties.origin, - href: urlProperties.href, - host: urlProperties.host, - hash: urlProperties.hash, - } - - const { searchParams, pathname } = urlPropertiesObject + const { searchParams, pathname } = urlProperties const isGraphQL = !config.graphQL.disable && pathname === `/api${config.routes.graphQL}` @@ -98,11 +84,15 @@ export const createPayloadRequest = async ({ transactionID: undefined, payloadDataLoader: undefined, payloadUploadSizes: {}, - host: urlProperties.host, - protocol: urlProperties.protocol, - pathname: urlProperties.pathname, searchParams: urlProperties.searchParams, + pathname: urlProperties.pathname, + port: urlProperties.port, + protocol: urlProperties.protocol, + search: urlProperties.search, origin: urlProperties.origin, + href: urlProperties.href, + host: urlProperties.host, + hash: urlProperties.hash, } const req: PayloadRequest = Object.assign(request, customRequest) From 366db1623b8a905d93b3dd53a4ba980f2c960435 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Fri, 16 Feb 2024 09:08:37 -0500 Subject: [PATCH 5/5] chore: passing graphql test suite --- .../src/utilities/createPayloadRequest.ts | 5 +- packages/payload/src/exports/utilities.ts | 10 +- packages/payload/src/types/index.ts | 5 +- test/access-control/int.spec.ts | 11 +- test/array-update/config.ts | 3 +- test/array-update/int.spec.ts | 24 +- test/array-update/shared.ts | 1 + test/auth/int.spec.ts | 9 - test/collections-graphql/config.ts | 8 +- test/collections-graphql/int.spec.ts | 545 ++++++++++-------- test/helpers/NextRESTClient.ts | 19 +- 11 files changed, 362 insertions(+), 278 deletions(-) create mode 100644 test/array-update/shared.ts diff --git a/packages/next/src/utilities/createPayloadRequest.ts b/packages/next/src/utilities/createPayloadRequest.ts index d2cb3306c9..eed34d52a1 100644 --- a/packages/next/src/utilities/createPayloadRequest.ts +++ b/packages/next/src/utilities/createPayloadRequest.ts @@ -12,6 +12,7 @@ import { initI18n } from '@payloadcms/translations' import { getRequestLanguage } from './getRequestLanguage' import { getRequestLocales } from './getRequestLocales' import { getDataAndFile } from './getDataAndFile' +import { getDataLoader } from 'payload/utilities' type Args = { request: Request @@ -38,7 +39,8 @@ export const createPayloadRequest = async ({ const urlProperties = new URL(request.url) const { searchParams, pathname } = urlProperties - const isGraphQL = !config.graphQL.disable && pathname === `/api${config.routes.graphQL}` + const isGraphQL = + !config.graphQL.disable && pathname === `${config.routes.api}${config.routes.graphQL}` const { data, file } = await getDataAndFile({ request, @@ -96,6 +98,7 @@ export const createPayloadRequest = async ({ } const req: PayloadRequest = Object.assign(request, customRequest) + req.payloadDataLoader = getDataLoader(req) req.user = await getAuthenticatedUser({ payload, diff --git a/packages/payload/src/exports/utilities.ts b/packages/payload/src/exports/utilities.ts index 2aad1fb61d..7c8c8ebbd9 100644 --- a/packages/payload/src/exports/utilities.ts +++ b/packages/payload/src/exports/utilities.ts @@ -1,34 +1,36 @@ +export { getDataLoader } from '../collections/dataloader' export { default as getDefaultValue } from '../fields/getDefaultValue' export { promise as afterReadPromise } from '../fields/hooks/afterRead/promise' export { traverseFields as afterReadTraverseFields } from '../fields/hooks/afterRead/traverseFields' export { extractTranslations } from '../translations/extractTranslations' + export { default as isImage } from '../uploads/isImage' export { combineMerge } from '../utilities/combineMerge' - export { configToJSONSchema, entityToJSONSchema, withNullableJSONSchemaType, } from '../utilities/configToJSONSchema' -export { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated' +export { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated' export { deepCopyObject } from '../utilities/deepCopyObject' export { deepMerge } from '../utilities/deepMerge' export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON' export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFields' export { formatLabels, formatNames, toWords } from '../utilities/formatLabels' -export { getIDType } from '../utilities/getIDType' +export { getIDType } from '../utilities/getIDType' export { getObjectDotNotation } from '../utilities/getObjectDotNotation' + export { default as getUniqueListBy } from '../utilities/getUniqueListBy' export { isNumber } from '../utilities/isNumber' - export { isValidID } from '../utilities/isValidID' export { setsAreEqual } from '../utilities/setsAreEqual' export { splitPathByArrayFields } from '../utilities/splitPathByArrayFields' export { default as toKebabCase } from '../utilities/toKebabCase' + export { default as wait } from '../utilities/wait' export { default as wordBoundariesRegex } from '../utilities/wordBoundariesRegex' diff --git a/packages/payload/src/types/index.ts b/packages/payload/src/types/index.ts index 5bc58ff060..94e76234ea 100644 --- a/packages/payload/src/types/index.ts +++ b/packages/payload/src/types/index.ts @@ -66,7 +66,10 @@ export type CustomPayloadRequest = { transactionIDPromise?: Promise /** The signed in user */ user: (U & User) | null -} & Pick +} & Pick< + URL, + 'hash' | 'host' | 'href' | 'origin' | 'pathname' | 'port' | 'protocol' | 'search' | 'searchParams' +> export type PayloadRequest = Partial & Required> & CustomPayloadRequest diff --git a/test/access-control/int.spec.ts b/test/access-control/int.spec.ts index c9b7c58a32..cda354f69e 100644 --- a/test/access-control/int.spec.ts +++ b/test/access-control/int.spec.ts @@ -1,9 +1,11 @@ +import type { Payload } from '../../packages/payload/src' import type { PayloadRequest } from '../../packages/payload/src/types' import type { Post, RelyOnRequestHeader, Restricted } from './payload-types' -import payload from '../../packages/payload/src' +import { getPayload } from '../../packages/payload/src' import { Forbidden } from '../../packages/payload/src/errors' -import { initPayloadTest } from '../helpers/configHelpers' +import { startMemoryDB } from '../startMemoryDB' +import configPromise from './config' import { requestHeaders } from './config' import { firstArrayText, @@ -17,12 +19,15 @@ import { slug, } from './shared' +let payload: Payload + describe('Access Control', () => { let post1: Post let restricted: Restricted beforeAll(async () => { - await initPayloadTest({ __dirname }) + const config = await startMemoryDB(configPromise) + payload = await getPayload({ config }) }) beforeEach(async () => { diff --git a/test/array-update/config.ts b/test/array-update/config.ts index 47ad4de80c..4300c4885e 100644 --- a/test/array-update/config.ts +++ b/test/array-update/config.ts @@ -1,10 +1,11 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults' import { devUser } from '../credentials' +import { arraySlug } from './shared' export default buildConfigWithDefaults({ collections: [ { - slug: 'arrays', + slug: arraySlug, fields: [ { name: 'arrayOfFields', diff --git a/test/array-update/int.spec.ts b/test/array-update/int.spec.ts index e7548da19e..59d2ea7ecc 100644 --- a/test/array-update/int.spec.ts +++ b/test/array-update/int.spec.ts @@ -1,14 +1,16 @@ -import payload from '../../packages/payload/src' -import { initPayloadTest } from '../helpers/configHelpers' -import configPromise from './config' +import type { Payload } from '../../packages/payload/src' -let collection: string +import { getPayload } from '../../packages/payload/src' +import { startMemoryDB } from '../startMemoryDB' +import configPromise from './config' +import { arraySlug } from './shared' + +let payload: Payload describe('array-update', () => { beforeAll(async () => { - const config = await configPromise - collection = config.collections[0]?.slug - await initPayloadTest({ __dirname }) + const config = await startMemoryDB(configPromise) + payload = await getPayload({ config }) }) afterAll(async () => { @@ -21,7 +23,7 @@ describe('array-update', () => { const originalText = 'some optional text' const doc = await payload.create({ - collection, + collection: arraySlug, data: { arrayOfFields: [ { @@ -47,7 +49,7 @@ describe('array-update', () => { const updatedDoc = await payload.update({ id: doc.id, - collection, + collection: arraySlug, data: { arrayOfFields: arrayWithExistingValues, }, @@ -68,7 +70,7 @@ describe('array-update', () => { } const doc = await payload.create({ - collection, + collection: arraySlug, data: { arrayOfFields: [ { @@ -82,7 +84,7 @@ describe('array-update', () => { const updatedDoc = await payload.update({ id: doc.id, - collection, + collection: arraySlug, data: { arrayOfFields: [ { diff --git a/test/array-update/shared.ts b/test/array-update/shared.ts new file mode 100644 index 0000000000..a4341dada0 --- /dev/null +++ b/test/array-update/shared.ts @@ -0,0 +1 @@ +export const arraySlug = 'arrays' diff --git a/test/auth/int.spec.ts b/test/auth/int.spec.ts index d929e21748..debd4c8c46 100644 --- a/test/auth/int.spec.ts +++ b/test/auth/int.spec.ts @@ -71,15 +71,6 @@ describe('Auth', () => { }) describe('REST - admin user', () => { - beforeAll(async () => { - await restClient.POST(`/${slug}/first-register`, { - body: JSON.stringify({ - email, - password, - }), - }) - }) - it('should prevent registering a new first user', async () => { const response = await restClient.POST(`/${slug}/first-register`, { body: JSON.stringify({ diff --git a/test/collections-graphql/config.ts b/test/collections-graphql/config.ts index f667576ba5..cace7429be 100644 --- a/test/collections-graphql/config.ts +++ b/test/collections-graphql/config.ts @@ -286,6 +286,7 @@ export default buildConfigWithDefaults({ }, }, { + slug: 'payload-api-test-ones', access: { read: () => true, }, @@ -298,9 +299,9 @@ export default buildConfigWithDefaults({ type: 'text', }, ], - slug: 'payload-api-test-ones', }, { + slug: 'payload-api-test-twos', access: { read: () => true, }, @@ -318,7 +319,6 @@ export default buildConfigWithDefaults({ type: 'relationship', }, ], - slug: 'payload-api-test-twos', }, { access: { @@ -328,7 +328,7 @@ export default buildConfigWithDefaults({ { name: 'contentType', hooks: { - afterRead: [({ req }) => req.headers?.['content-type']], + afterRead: [({ req }) => req.headers?.get('content-type')], }, type: 'text', }, @@ -483,7 +483,7 @@ export default buildConfigWithDefaults({ data: {}, }) - await payload.create({ + const t = await payload.create({ collection: 'payload-api-test-twos', data: { relation: payloadAPITest1.id, diff --git a/test/collections-graphql/int.spec.ts b/test/collections-graphql/int.spec.ts index d5f1f4ff04..710dee8abc 100644 --- a/test/collections-graphql/int.spec.ts +++ b/test/collections-graphql/int.spec.ts @@ -1,34 +1,35 @@ -import { GraphQLClient } from 'graphql-request' - +import type { Payload } from '../../packages/payload/src' import type { Post } from './payload-types' -import payload from '../../packages/payload/src' +import { getPayload } from '../../packages/payload/src' import { mapAsync } from '../../packages/payload/src/utilities/mapAsync' -import { initPayloadTest } from '../helpers/configHelpers' +import { NextRESTClient } from '../helpers/NextRESTClient' import { idToString } from '../helpers/idToString' +import { startMemoryDB } from '../startMemoryDB' import configPromise, { errorOnHookSlug, pointSlug, relationSlug, slug } from './config' const title = 'title' -let client: GraphQLClient +let restClient: NextRESTClient +let payload: Payload describe('collections-graphql', () => { beforeAll(async () => { - const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } }) - const config = await configPromise - const url = `${serverURL}${config.routes.api}${config.routes.graphQL}` - client = new GraphQLClient(url) + const config = await startMemoryDB(configPromise) + payload = await getPayload({ config }) + restClient = new NextRESTClient(payload.config) + // TODO: reenable when we migrate back to mongoose v6 // Wait for indexes to be created, // as we need them to query by point - if (payload.db.name === 'mongoose') { - await new Promise((resolve, reject) => { - payload.db?.collections?.point?.ensureIndexes(function (err) { - if (err) reject(err) - resolve(true) - }) - }) - } + // if (payload.db.name === 'mongoose') { + // await new Promise((resolve, reject) => { + // payload.db?.collections?.point?.ensureIndexes(function (err) { + // if (err) reject(err) + // resolve(true) + // }) + // }) + // } }) afterAll(async () => { @@ -53,8 +54,11 @@ describe('collections-graphql', () => { title } }` - const response = await client.request(query) - const doc: Post = response.createPost + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + + const doc: Post = data.createPost expect(doc).toMatchObject({ title }) expect(doc.id).toBeDefined() @@ -67,8 +71,16 @@ describe('collections-graphql', () => { title } }` - const response = (await client.request(query, { title })) as any - const doc: Post = response.createPost + const { data } = await restClient + .GRAPHQL_POST({ + body: JSON.stringify({ + query, + variables: { title }, + }), + }) + .then((res) => res.json()) + + const doc: Post = data.createPost expect(doc).toMatchObject({ title }) expect(doc.id).toBeDefined() @@ -81,8 +93,10 @@ describe('collections-graphql', () => { title } }` - const response = await client.request(query) - const doc: Post = response.Post + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const doc: Post = data.Post expect(doc).toMatchObject({ id: existingDoc.id, title }) }) @@ -96,8 +110,10 @@ describe('collections-graphql', () => { } } }` - const response = await client.request(query) - const { docs } = response.Posts + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts expect(docs).toContainEqual(expect.objectContaining({ id: existingDoc.id })) }) @@ -120,8 +136,10 @@ describe('collections-graphql', () => { title } }` - const response = await client.request(query) - const { postIDs, posts, singlePost } = response + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { postIDs, posts, singlePost } = data expect(postIDs.docs).toBeDefined() expect(posts.docs).toBeDefined() expect(singlePost.id).toBeDefined() @@ -167,12 +185,12 @@ describe('collections-graphql', () => { } }` - client.requestConfig.errorPolicy = 'all' - const response = await client.request(query) - client.requestConfig.errorPolicy = 'none' + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) const createdResult = await payload.findByID({ - id: response.createPost.id, + id: data.createPost.id, collection: slug, }) const updateFirstResult = await payload.findByID({ @@ -184,11 +202,11 @@ describe('collections-graphql', () => { collection: errorOnHookSlug, }) - expect(response?.createPost.id).toBeDefined() - expect(response?.updateFirst).toBeNull() - expect(response?.updateSecond).toBeNull() + expect(data?.createPost.id).toBeDefined() + expect(data?.updateFirst).toBeNull() + expect(data?.updateSecond).toBeNull() - expect(createdResult).toMatchObject(response.createPost) + expect(createdResult).toMatchObject(data.createPost) expect(updateFirstResult).toMatchObject(first) expect(updateSecondResult).toStrictEqual(second) }) @@ -198,6 +216,7 @@ describe('collections-graphql', () => { query { PayloadApiTestTwos { docs { + id payloadAPI relation { payloadAPI @@ -206,9 +225,10 @@ describe('collections-graphql', () => { } } ` - - const response = await client.request(query) - const res = response.PayloadApiTestTwos + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const res = data.PayloadApiTestTwos expect(res.docs[0].relation.payloadAPI).toStrictEqual('GraphQL') }) @@ -221,8 +241,10 @@ describe('collections-graphql', () => { } } }` - const response = await client.request(query) - expect(response.ContentTypes?.docs[0]?.contentType).toEqual('application/json') + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + expect(data.ContentTypes?.docs[0]?.contentType).toEqual('application/json') }) it('should update existing', async () => { @@ -234,8 +256,10 @@ describe('collections-graphql', () => { title } }` - const response = await client.request(query) - const doc: Post = response.updatePost + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const doc: Post = data.updatePost expect(doc).toMatchObject({ id: existingDoc.id, title: updatedTitle }) }) @@ -247,8 +271,14 @@ describe('collections-graphql', () => { title } }` - const response = await client.request(query) - const doc: Post = response.deletePost + const { data } = await restClient + .GRAPHQL_POST({ + body: JSON.stringify({ + query, + }), + }) + .then((res) => res.json()) + const doc: Post = data.deletePost expect(doc).toMatchObject({ id: existingDoc.id }) }) @@ -266,32 +296,35 @@ describe('collections-graphql', () => { it('equals', async () => { const query = `query { - Posts(where:{title: {equals:"${post1.title}"}}) { - docs { - id - title + Posts(where:{title: {equals:"${post1.title}"}}) { + docs { + id + title + } } - } - }` - - const response = await client.request(query) - const { docs } = response.Posts + }` + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts expect(docs).toContainEqual(expect.objectContaining({ id: post1.id, title: post1.title })) }) it('not_equals', async () => { const query = `query { - Posts(where:{title: {not_equals:"${post1.title}"}}) { - docs { - id - title + Posts(where:{title: {not_equals:"${post1.title}"}}) { + docs { + id + title + } } - } - }` + }` - const response = await client.request(query) - const { docs } = response.Posts + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts const docsWithWhereTitleNotEqualPostTitle = docs.filter( (post) => post.title === post1.title, ) @@ -302,32 +335,36 @@ describe('collections-graphql', () => { it('like', async () => { const postWithWords = await createPost({ title: 'the quick brown fox' }) const query = `query { - Posts(where:{title: {like:"${postWithWords.title?.split(' ')[1]}"}}) { - docs { - id - title + Posts(where:{title: {like:"${postWithWords.title?.split(' ')[1]}"}}) { + docs { + id + title + } } - } - }` + }` - const response = await client.request(query) - const { docs } = response.Posts + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts expect(docs[0]).toMatchObject({ id: postWithWords.id, title: postWithWords.title }) }) it('contains', async () => { const query = `query { - Posts(where:{title: {contains:"${post1.title?.slice(0, 4)}"}}) { - docs { - id - title + Posts(where:{title: {contains:"${post1.title?.slice(0, 4)}"}}) { + docs { + id + title + } } - } - }` + }` - const response = await client.request(query) - const { docs } = response.Posts + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts expect(docs).toContainEqual(expect.objectContaining({ id: post1.id, title: post1.title })) expect(docs).toContainEqual(expect.objectContaining({ id: post2.id, title: post2.title })) @@ -336,16 +373,18 @@ describe('collections-graphql', () => { it('exists - true', async () => { const withDescription = await createPost({ description: 'description' }) const query = `query { - Posts(where:{description: {exists:true}}) { - docs { - id - title + Posts(where:{description: {exists:true}}) { + docs { + id + title + } } - } - }` + }` - const response = await client.request(query) - const { docs } = response.Posts + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts expect(docs).toContainEqual( expect.objectContaining({ id: withDescription.id, title: withDescription.title }), @@ -355,16 +394,17 @@ describe('collections-graphql', () => { it('exists - false', async () => { const withDescription = await createPost({ description: 'description' }) const query = `query { - Posts(where:{description: {exists:false}}) { - docs { - id - title + Posts(where:{description: {exists:false}}) { + docs { + id + title + } } - } - }` - - const response = await client.request(query) - const { docs } = response.Posts + }` + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts expect(docs).not.toContainEqual(expect.objectContaining({ id: withDescription.id })) expect(docs).toContainEqual(expect.objectContaining({ id: post1.id })) @@ -381,32 +421,34 @@ describe('collections-graphql', () => { it('greater_than', async () => { const query = `query { - Posts(where:{number: {greater_than:1}}) { - docs { - id - title - number + Posts(where:{number: {greater_than:1}}) { + docs { + id + title + number + } } - } - }` - - const response = await client.request(query) - const { docs } = response.Posts + }` + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts expect(docs.map(({ id }) => id)).toContain(numPost2.id) }) it('greater_than_equal', async () => { const query = `query { - Posts(where:{number: {greater_than_equal:1}}) { - docs { - id - title + Posts(where:{number: {greater_than_equal:1}}) { + docs { + id + title + } } - } - }` - - const response = await client.request(query) - const { docs } = response.Posts + }` + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts expect(docs).toContainEqual(expect.objectContaining({ id: numPost1.id })) expect(docs).toContainEqual(expect.objectContaining({ id: numPost2.id })) @@ -414,32 +456,36 @@ describe('collections-graphql', () => { it('less_than', async () => { const query = `query { - Posts(where:{number: {less_than:2}}) { - docs { - id - title + Posts(where:{number: {less_than:2}}) { + docs { + id + title + } } - } - }` + }` - const response = await client.request(query) - const { docs } = response.Posts + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts expect(docs).toContainEqual(expect.objectContaining({ id: numPost1.id })) }) it('less_than_equal', async () => { const query = `query { - Posts(where:{number: {less_than_equal:2}}) { - docs { - id - title + Posts(where:{number: {less_than_equal:2}}) { + docs { + id + title + } } - } - }` + }` - const response = await client.request(query) - const { docs } = response.Posts + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts expect(docs).toContainEqual(expect.objectContaining({ id: numPost1.id })) expect(docs).toContainEqual(expect.objectContaining({ id: numPost2.id })) @@ -448,18 +494,20 @@ describe('collections-graphql', () => { it('or', async () => { const query = `query { - Posts( - where: {OR: [{ title: { equals: "${post1.title}" } }, { title: { equals: "${post2.title}" } }] - }) { - docs { - id - title + Posts( + where: {OR: [{ title: { equals: "${post1.title}" } }, { title: { equals: "${post2.title}" } }] + }) { + docs { + id + title + } } - } - }` + }` - const response = await client.request(query) - const { docs } = response.Posts + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts expect(docs).toContainEqual(expect.objectContaining({ id: post1.id })) expect(docs).toContainEqual(expect.objectContaining({ id: post2.id })) @@ -467,18 +515,20 @@ describe('collections-graphql', () => { it('or - 1 result', async () => { const query = `query { - Posts( - where: {OR: [{ title: { equals: "${post1.title}" } }, { title: { equals: "nope" } }] - }) { - docs { - id - title + Posts( + where: {OR: [{ title: { equals: "${post1.title}" } }, { title: { equals: "nope" } }] + }) { + docs { + id + title + } } - } - }` + }` - const response = await client.request(query) - const { docs } = response.Posts + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts expect(docs).toContainEqual(expect.objectContaining({ id: post1.id })) expect(docs).not.toContainEqual(expect.objectContaining({ id: post2.id })) @@ -488,22 +538,24 @@ describe('collections-graphql', () => { const specialPost = await createPost({ description: 'special-123123' }) const query = `query { - Posts( - where: { - AND: [ - { title: { equals: "${specialPost.title}" } } - { description: { equals: "${specialPost.description}" } } - ] - }) { - docs { - id - title + Posts( + where: { + AND: [ + { title: { equals: "${specialPost.title}" } } + { description: { equals: "${specialPost.description}" } } + ] + }) { + docs { + id + title + } } - } - }` + }` - const response = await client.request(query) - const { docs } = response.Posts + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts expect(docs).toContainEqual(expect.objectContaining({ id: specialPost.id })) }) @@ -530,8 +582,10 @@ describe('collections-graphql', () => { } }` - const response = await client.request(nearQuery) - const { docs } = response.Points + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query: nearQuery }) }) + .then((res) => res.json()) + const { docs } = data.Points expect(docs).toHaveLength(1) }) @@ -553,8 +607,10 @@ describe('collections-graphql', () => { } }` - const response = await client.request(nearQuery) - const { docs } = response.Points + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query: nearQuery }) }) + .then((res) => res.json()) + const { docs } = data.Points expect(docs).toHaveLength(0) }) @@ -591,8 +647,10 @@ describe('collections-graphql', () => { } }` - const response = await client.request(nearQuery) - const { docs } = response.Points + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query: nearQuery }) }) + .then((res) => res.json()) + const { docs } = data.Points let previous = 0 docs.forEach(({ point: coordinates }) => { @@ -632,8 +690,10 @@ describe('collections-graphql', () => { } }` - const response = await client.request(query) - const { docs } = response.Points + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Points expect(docs).toHaveLength(1) expect(docs[0].point).toEqual([10, 20]) @@ -659,8 +719,10 @@ describe('collections-graphql', () => { } }` - const response = await client.request(query) - const { docs } = response.Points + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Points expect(docs).toHaveLength(0) }) @@ -695,8 +757,10 @@ describe('collections-graphql', () => { } }` - const response = await client.request(query) - const { docs } = response.Points + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Points expect(docs).toHaveLength(1) expect(docs[0].point).toEqual([10, 20]) @@ -722,8 +786,10 @@ describe('collections-graphql', () => { } }` - const response = await client.request(query) - const { docs } = response.Points + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Points expect(docs).toHaveLength(0) }) @@ -746,8 +812,10 @@ describe('collections-graphql', () => { } } }` - const response = await client.request(query) - const { docs } = response.Posts + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts expect(docs).toContainEqual( expect.objectContaining({ @@ -773,8 +841,10 @@ describe('collections-graphql', () => { } }` - const response = await client.request(query) - const { docs, totalDocs } = response.Posts + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs, totalDocs } = data.Posts expect(totalDocs).toStrictEqual(1) expect(docs[0].relationToCustomID.id).toStrictEqual(1) @@ -814,8 +884,10 @@ describe('collections-graphql', () => { } }` - const response = await client.request(query) - const { docs } = response.Posts + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts expect(docs[0].relationField).toBeFalsy() }) @@ -854,8 +926,10 @@ describe('collections-graphql', () => { } }` - const response = await client.request(query) - const { docs } = response.Posts + const { data } = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const { docs } = data.Posts expect(docs[0].relationHasManyField).toHaveLength(0) }) @@ -864,9 +938,6 @@ describe('collections-graphql', () => { describe('Error Handler', () => { it('should return have an array of errors when making a bad request', async () => { - let error - - // language=graphQL const query = `query { Posts(where: { title: { exists: true }}) { docs { @@ -874,16 +945,16 @@ describe('collections-graphql', () => { } } }` - await client.request(query).catch((err) => { - error = err - }) - expect(Array.isArray(error.response.errors)).toBe(true) - expect(typeof error.response.errors[0].message).toBe('string') + const { errors } = await restClient + .GRAPHQL_POST({ + body: JSON.stringify({ query }), + }) + .then((res) => res.json()) + expect(Array.isArray(errors)).toBe(true) + expect(typeof errors[0].message).toBe('string') }) it('should return have an array of errors when failing to pass validation', async () => { - let error - // language=graphQL const query = `mutation { createPost(data: {min: 1}) { id @@ -893,17 +964,17 @@ describe('collections-graphql', () => { } }` - await client.request(query).catch((err) => { - error = err - }) - expect(Array.isArray(error.response.errors)).toBe(true) - expect(error.response.errors[0].message).toEqual('The following field is invalid: min') - expect(typeof error.response.errors[0].locations).toBeDefined() + const { errors } = await restClient + .GRAPHQL_POST({ + body: JSON.stringify({ query }), + }) + .then((res) => res.json()) + expect(Array.isArray(errors)).toBe(true) + expect(errors[0].message).toEqual('The following field is invalid: min') + expect(typeof errors[0].locations).toBeDefined() }) it('should return have an array of errors when failing multiple mutations', async () => { - let error - // language=graphQL const query = `mutation createTest { test1:createUser(data: { email: "test@test.com", password: "test" }) { email @@ -922,36 +993,36 @@ describe('collections-graphql', () => { } }` - await client.request(query).catch((err) => { - error = err - }) + const { errors } = await restClient + .GRAPHQL_POST({ + body: JSON.stringify({ query }), + }) + .then((res) => res.json()) - expect(Array.isArray(error.response.errors)).toBe(true) + expect(Array.isArray(errors)).toBe(true) - expect(Array.isArray(error.response.errors[0].locations)).toEqual(true) - expect(error.response.errors[0].message).toEqual('The following field is invalid: password') - expect(error.response.errors[0].path[0]).toEqual('test2') - expect(error.response.errors[0].extensions.name).toEqual('ValidationError') - expect(error.response.errors[0].extensions.data[0].message).toEqual('No password was given') - expect(error.response.errors[0].extensions.data[0].field).toEqual('password') + expect(Array.isArray(errors[0].locations)).toEqual(true) + expect(errors[0].message).toEqual('The following field is invalid: password') + expect(errors[0].path[0]).toEqual('test2') + expect(errors[0].extensions.name).toEqual('ValidationError') + expect(errors[0].extensions.data[0].message).toEqual('No password was given') + expect(errors[0].extensions.data[0].field).toEqual('password') - expect(Array.isArray(error.response.errors[1].locations)).toEqual(true) - expect(error.response.errors[1].message).toEqual('The following field is invalid: email') - expect(error.response.errors[1].path[0]).toEqual('test3') - expect(error.response.errors[1].extensions.name).toEqual('ValidationError') - expect(error.response.errors[1].extensions.data[0].message).toEqual( + expect(Array.isArray(errors[1].locations)).toEqual(true) + expect(errors[1].message).toEqual('The following field is invalid: email') + expect(errors[1].path[0]).toEqual('test3') + expect(errors[1].extensions.name).toEqual('ValidationError') + expect(errors[1].extensions.data[0].message).toEqual( 'A user with the given email is already registered', ) - expect(error.response.errors[1].extensions.data[0].field).toEqual('email') + expect(errors[1].extensions.data[0].field).toEqual('email') - expect(Array.isArray(error.response.errors[2].locations)).toEqual(true) - expect(error.response.errors[2].message).toEqual('The following field is invalid: email') - expect(error.response.errors[2].path[0]).toEqual('test4') - expect(error.response.errors[2].extensions.name).toEqual('ValidationError') - expect(error.response.errors[2].extensions.data[0].message).toEqual( - 'Please enter a valid email address.', - ) - expect(error.response.errors[2].extensions.data[0].field).toEqual('email') + expect(Array.isArray(errors[2].locations)).toEqual(true) + expect(errors[2].message).toEqual('The following field is invalid: email') + expect(errors[2].path[0]).toEqual('test4') + expect(errors[2].extensions.name).toEqual('ValidationError') + expect(errors[2].extensions.data[0].message).toEqual('Please enter a valid email address.') + expect(errors[2].extensions.data[0].field).toEqual('email') }) it('should return the minimum allowed information about internal errors', async () => { @@ -963,16 +1034,18 @@ describe('collections-graphql', () => { } }` - await client.request(query).catch((err) => { - error = err - }) + const { errors } = await restClient + .GRAPHQL_POST({ + body: JSON.stringify({ query }), + }) + .then((res) => res.json()) - expect(Array.isArray(error.response.errors)).toBe(true) - expect(Array.isArray(error.response.errors[0].locations)).toEqual(true) - expect(error.response.errors[0].message).toEqual('Something went wrong.') - expect(error.response.errors[0].path[0]).toEqual('QueryWithInternalError') - expect(error.response.errors[0].extensions.statusCode).toEqual(500) - expect(error.response.errors[0].extensions.name).toEqual('Error') + expect(Array.isArray(errors)).toBe(true) + expect(Array.isArray(errors[0].locations)).toEqual(true) + expect(errors[0].message).toEqual('Something went wrong.') + expect(errors[0].path[0]).toEqual('QueryWithInternalError') + expect(errors[0].extensions.statusCode).toEqual(500) + expect(errors[0].extensions.name).toEqual('Error') }) }) }) diff --git a/test/helpers/NextRESTClient.ts b/test/helpers/NextRESTClient.ts index 9597c4df01..07689e91de 100644 --- a/test/helpers/NextRESTClient.ts +++ b/test/helpers/NextRESTClient.ts @@ -61,14 +61,17 @@ export class NextRESTClient { } async GRAPHQL_POST(options: RequestInit): Promise { - const request = new Request(`${this.serverURL}${this.config.routes.graphQL}`, { - ...options, - method: 'POST', - headers: new Headers({ - 'Content-Type': 'application/json', - ...(options?.headers || {}), - }), - }) + const request = new Request( + `${this.serverURL}${this.config.routes.api}${this.config.routes.graphQL}`, + { + ...options, + method: 'POST', + headers: new Headers({ + 'Content-Type': 'application/json', + ...(options?.headers || {}), + }), + }, + ) return this._GRAPHQL_POST(request) }