Files
payload/test/helpers/NextRESTClient.ts
Dan Ribbens 6ef2bdea15 feat!: join field (#7518)
## Description

- Adds a new "join" field type to Payload and is supported by all database adapters
- The UI uses a table view for the new field
- `db-mongodb` changes relationships to be stored as ObjectIDs instead of strings (for now querying works using both types internally to the DB so no data migration should be necessary unless you're querying directly, see breaking changes for details
- Adds a reusable traverseFields utility to Payload to make it easier to work with nested fields, used internally and for plugin maintainers

```ts
export const Categories: CollectionConfig = {
    slug: 'categories',
    fields: [
        {
            name: 'relatedPosts',
            type: 'join',
            collection: 'posts',
            on: 'category',
        }
    ]
}
```

BREAKING CHANGES:
All mongodb relationship and upload values will be stored as MongoDB ObjectIDs instead of strings going forward. If you have existing data and you are querying data directly, outside of Payload's APIs, you get different results. For example, a `contains` query will no longer works given a partial ID of a relationship since the ObjectID requires the whole identifier to work. 

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
Co-authored-by: James <james@trbl.design>
2024-09-20 11:10:16 -04:00

210 lines
5.8 KiB
TypeScript

import type { JoinQuery, SanitizedConfig, Where } from 'payload'
import type { ParsedQs } from 'qs-esm'
import {
REST_DELETE as createDELETE,
REST_GET as createGET,
GRAPHQL_POST as createGraphqlPOST,
REST_PATCH as createPATCH,
REST_POST as createPOST,
} from '@payloadcms/next/routes'
import * as qs from 'qs-esm'
import { devUser } from '../credentials.js'
type ValidPath = `/${string}`
type RequestOptions = {
auth?: boolean
query?: {
depth?: number
fallbackLocale?: string
joins?: JoinQuery
limit?: number
locale?: string
page?: number
sort?: string
where?: Where
}
}
type FileArg = {
file?: Omit<File, 'webkitRelativePath'>
}
function generateQueryString(query: RequestOptions['query'], params: ParsedQs): string {
return qs.stringify(
{
...(params || {}),
...(query || {}),
},
{
addQueryPrefix: true,
},
)
}
export class NextRESTClient {
private _DELETE: (request: Request, args: { params: { slug: string[] } }) => Promise<Response>
private _GET: (request: Request, args: { params: { slug: string[] } }) => Promise<Response>
private _GRAPHQL_POST: (request: Request) => Promise<Response>
private _PATCH: (request: Request, args: { params: { slug: string[] } }) => Promise<Response>
private _POST: (request: Request, args: { params: { slug: string[] } }) => Promise<Response>
private readonly config: SanitizedConfig
private token: string
serverURL: string = 'http://localhost:3000'
constructor(config: SanitizedConfig) {
this.config = config
if (config?.serverURL) this.serverURL = config.serverURL
this._GET = createGET(config)
this._POST = createPOST(config)
this._DELETE = createDELETE(config)
this._PATCH = createPATCH(config)
this._GRAPHQL_POST = createGraphqlPOST(config)
}
private buildHeaders(options: FileArg & RequestInit & RequestOptions): Headers {
const defaultHeaders = {
'Content-Type': 'application/json',
}
const headers = new Headers({
...(options?.file
? {
'Content-Length': options.file.size.toString(),
}
: defaultHeaders),
...(options?.headers || {}),
})
if (options.auth !== false && this.token) {
headers.set('Authorization', `JWT ${this.token}`)
}
if (options.auth === false) {
headers.set('DisableAutologin', 'true')
}
return headers
}
private generateRequestParts(path: ValidPath): {
params?: ParsedQs
slug: string[]
url: string
} {
const [slugs, params] = path.slice(1).split('?')
const url = `${this.serverURL}${this.config.routes.api}/${slugs}`
return {
slug: slugs.split('/'),
params: params ? qs.parse(params) : undefined,
url,
}
}
async DELETE(path: ValidPath, options: RequestInit & RequestOptions = {}): Promise<Response> {
const { slug, params, url } = this.generateRequestParts(path)
const { query, ...rest } = options || {}
const queryParams = generateQueryString(query, params)
const request = new Request(`${url}${queryParams}`, {
...rest,
headers: this.buildHeaders(options),
method: 'DELETE',
})
return this._DELETE(request, { params: { slug } })
}
async GET(
path: ValidPath,
options: Omit<RequestInit, 'body'> & RequestOptions = {},
): Promise<Response> {
const { slug, params, url } = this.generateRequestParts(path)
const { query, ...rest } = options || {}
const queryParams = generateQueryString(query, params)
const request = new Request(`${url}${queryParams}`, {
...rest,
headers: this.buildHeaders(options),
method: 'GET',
})
return this._GET(request, { params: { slug } })
}
async GRAPHQL_POST(options: RequestInit & RequestOptions): Promise<Response> {
const { query, ...rest } = options
const queryParams = generateQueryString(query, {})
const request = new Request(
`${this.serverURL}${this.config.routes.api}${this.config.routes.graphQL}${queryParams}`,
{
...rest,
headers: this.buildHeaders(options),
method: 'POST',
},
)
return this._GRAPHQL_POST(request)
}
async PATCH(path: ValidPath, options: FileArg & RequestInit & RequestOptions): Promise<Response> {
const { slug, params, url } = this.generateRequestParts(path)
const { query, ...rest } = options
const queryParams = generateQueryString(query, params)
const request = new Request(`${url}${queryParams}`, {
...rest,
headers: this.buildHeaders(options),
method: 'PATCH',
})
return this._PATCH(request, { params: { slug } })
}
async POST(
path: ValidPath,
options: FileArg & RequestInit & RequestOptions = {},
): Promise<Response> {
const { slug, params, url } = this.generateRequestParts(path)
const queryParams = generateQueryString({}, params)
const request = new Request(`${url}${queryParams}`, {
...options,
headers: this.buildHeaders(options),
method: 'POST',
})
return this._POST(request, { params: { slug } })
}
async login({
slug,
credentials,
}: {
credentials?: {
email: string
password: string
}
slug: string
}): Promise<{ [key: string]: any }> {
const response = await this.POST(`/${slug}/login`, {
body: JSON.stringify(
credentials ? { ...credentials } : { email: devUser.email, password: devUser.password },
),
})
const result = await response.json()
this.token = result.token
if (!result.token) {
// If the token is not in the response body, then we can extract it from the cookies
const setCookie = response.headers.get('Set-Cookie')
const tokenMatchResult = setCookie?.match(/payload-token=(?<token>.+?);/)
this.token = tokenMatchResult?.groups?.token
}
return result
}
}