Compare commits

...

3 Commits

Author SHA1 Message Date
Sasha
6f9a9e8b62 test 2024-12-03 06:59:50 +02:00
Sasha
103dfe1ca0 feat: add findVersions findVersionByID restoreVersion findGlobalVersrions findGlobalVersionByID restoreGlobalVersion operations 2024-12-02 23:09:36 +02:00
Sasha
86da15a175 feat: add Payload SDK package 2024-12-02 22:03:34 +02:00
64 changed files with 3355 additions and 39 deletions

View File

@@ -59,6 +59,7 @@ jobs:
richtext-\*
richtext-lexical
richtext-slate
sdk
storage-\*
storage-azure
storage-gcs

View File

@@ -13,6 +13,8 @@ keywords: rest, api, documentation, Content Management System, cms, headless, ja
The REST API is a fully functional HTTP client that allows you to interact with your Documents in a RESTful manner. It supports all CRUD operations and is equipped with automatic pagination, depth, and sorting.
All Payload API routes are mounted and prefixed to your config's `routes.api` URL segment (default: `/api`).
To enhance DX, you can use [Payload SDK](#payload-rest-api-sdk) to query your REST API.
**REST query parameters:**
- [depth](../queries/depth) - automatically populates relationships and uploads
@@ -752,3 +754,243 @@ const res = await fetch(`${api}/${collectionSlug}?depth=1&locale=en`, {
},
})
```
## Payload REST API SDK
The best, fully type-safe way to query Payload REST API is to use its SDK client, which can be installed with:
```bash
pnpm add @payloadcms/sdk
```
Its usage is very similar to [the Local API](../local-api/overview).
Example:
```ts
import { PayloadSDK } from '@payloadcms/sdk'
import type { Config } from './payload-types'
// Pass your config from generated types as generic
const sdk = new PayloadSDK<Config>({
baseURL: 'https://example.com/api',
})
// Find operation
const posts = await sdk.find({
collection: 'posts',
draft: true,
limit: 10,
locale: 'en',
page: 1,
where: { _status: { equals: 'published' } },
})
// Find by ID operation
const posts = await sdk.findByID({
id,
collection: 'posts',
draft: true,
locale: 'en',
})
// Auth login operation
const result = await sdk.login({
collection: 'users',
data: {
email: 'dev@payloadcms.com',
password: '12345',
},
})
// Create operation
const result = await sdk.create({
collection: 'posts',
data: { text: 'text' },
})
// Create operation with a file
// `file` can be either a Blob | File object or a string URL
const result = await sdk.create({ collection: 'media', file, data: {} })
// Count operation
const result = await sdk.count({ collection: 'posts', where: { id: { equals: post.id } } })
// Update (by ID) operation
const result = await sdk.update({
collection: 'posts',
id: post.id,
data: {
text: 'updated-text',
},
})
// Update (bulk) operation
const result = await sdk.update({
collection: 'posts',
where: {
id: {
equals: post.id,
},
},
data: { text: 'updated-text-bulk' },
})
// Delete (by ID) operation
const result = await sdk.delete({ id: post.id, collection: 'posts' })
// Delete (bulk) operation
const result = await sdk.delete({ where: { id: { equals: post.id } }, collection: 'posts' })
// Find Global operation
const result = await sdk.findGlobal({ slug: 'global' })
// Update Global operation
const result = await sdk.updateGlobal({ slug: 'global', data: { text: 'some-updated-global' } })
// Auth Login operation
const result = await sdk.login({
collection: 'users',
data: { email: 'dev@payloadcms.com', password: '123456' },
})
// Auth Me operation
const result = await sdk.me(
{ collection: 'users' },
{
headers: {
Authorization: `JWT ${user.token}`,
},
},
)
// Auth Refresh Token operation
const result = await sdk.refreshToken(
{ collection: 'users' },
{ headers: { Authorization: `JWT ${user.token}` } },
)
// Auth Forgot Password operation
const result = await sdk.forgotPassword({
collection: 'users',
data: { email: user.email },
})
// Auth Reset Password operation
const result = await sdk.resetPassword({
collection: 'users',
data: { password: '1234567', token: resetPasswordToken },
})
// Find Versions operation
const result = await sdk.findVersions({
collection: 'posts',
where: { parent: { equals: post.id } },
})
// Find Version by ID operation
const result = await sdk.findVersionByID({ collection: 'posts', id: version.id })
// Restore Version operation
const result = await sdk.restoreVersion({
collection: 'posts',
id,
})
// Find Global Versions operation
const result = await sdk.findGlobalVersions({
slug: 'global',
})
// Find Global Version by ID operation
const result = await sdk.findGlobalVersionByID({ id: version.id, slug: 'global' })
// Restore Global Version operation
const result = await sdk.restoreGlobalVersion({
slug: 'global',
id
})
```
Every operation has optional 3rd parameter which is used to add additional data to the RequestInit object (like headers):
```ts
await sdk.me({
collection: "users"
}, {
// RequestInit object
headers: {
Authorization: `JWT ${token}`
}
})
```
To query custom endpoints, you can use the `request` method, which is used internally for all other methods:
```ts
await sdk.request({
method: 'POST',
path: '/send-data',
json: {
id: 1,
},
})
```
Custom `fetch` implementation and `baseInit` for shared `RequestInit` properties:
```ts
const sdk = new PayloadSDK<Config>({
baseInit: { credentials: 'include' },
baseURL: 'https://example.com/api',
fetch: async (url, init) => {
console.log('before req')
const response = await fetch(url, init)
console.log('after req')
return response
},
})
```
Example of a custom `fetch` implementation for testing the REST API without needing to spin up a next development server:
```ts
import type { GeneratedTypes, SanitizedConfig } from 'payload'
import config from '@payload-config'
import { REST_DELETE, REST_GET, REST_PATCH, REST_POST, REST_PUT } from '@payloadcms/next/routes'
import { PayloadSDK } from '@payloadcms/sdk'
export type TypedPayloadSDK = PayloadSDK<GeneratedTypes>
const api = {
GET: REST_GET(config),
POST: REST_POST(config),
PATCH: REST_PATCH(config),
DELETE: REST_DELETE(config),
PUT: REST_PUT(config),
}
const awaitedConfig = await config
export const sdk = new PayloadSDK<GeneratedTypes>({
baseURL: ``,
fetch: (path: string, init: RequestInit) => {
const [slugs, search] = path.slice(1).split('?')
const url = `${awaitedConfig.serverURL || 'http://localhost:3000'}${awaitedConfig.routes.api}/${slugs}${search ? `?${search}` : ''}`
if (init.body instanceof FormData) {
const file = init.body.get('file') as Blob
if (file && init.headers instanceof Headers) {
init.headers.set('Content-Length', file.size.toString())
}
}
const request = new Request(url, init)
const params = {
params: Promise.resolve({
slug: slugs.split('/'),
}),
}
return api[init.method.toUpperCase()](request, params)
},
})
```

View File

@@ -41,6 +41,7 @@
"build:plugins": "turbo build --filter \"@payloadcms/plugin-*\"",
"build:richtext-lexical": "turbo build --filter \"@payloadcms/richtext-lexical\"",
"build:richtext-slate": "turbo build --filter \"@payloadcms/richtext-slate\"",
"build:sdk": "turbo build --filter \"@payloadcms/sdk\"",
"build:storage-azure": "turbo build --filter \"@payloadcms/storage-azure\"",
"build:storage-gcs": "turbo build --filter \"@payloadcms/storage-gcs\"",
"build:storage-s3": "turbo build --filter \"@payloadcms/storage-s3\"",

123
packages/payload/src/cache/database.ts vendored Normal file
View File

@@ -0,0 +1,123 @@
import type { CollectionConfig } from '../collections/config/types.js'
import type { Payload, PayloadRequest } from '../types/index.js'
import type { PayloadCache, PayloadCacheConstructor } from './index.js'
const req = {} as PayloadRequest
export const databaseCacheAdapter = ({
collectionOverrides = {},
}: {
collectionOverrides?: Partial<CollectionConfig>
}): PayloadCacheConstructor => {
const constructor: PayloadCacheConstructor = class DatabaseCache<
Data extends Record<string, unknown> = Record<string, unknown>,
> implements PayloadCache<Data>
{
type = 'database'
constructor(private readonly payload: Payload) {}
async clear(): Promise<void> {
await this.payload.db.deleteMany({
collection: 'payload-cache',
req,
where: {},
})
}
async delete(key: string): Promise<void> {
await this.payload.db.deleteOne({
collection: 'payload-cache',
req,
where: { key: { equals: key } },
})
}
async getAll(): Promise<{ data: Data; key: string }[]> {
const { docs } = await this.payload.db.find<{
data: Data
key: string
}>({
collection: 'payload-cache',
joins: false,
limit: 0,
pagination: false,
req,
select: {
data: true,
key: true,
},
})
return docs
}
async getByKey(key: string): Promise<Data | null> {
const doc = await this.payload.db.findOne<{
data: Data
id: number | string
}>({
collection: 'payload-cache',
joins: false,
req,
select: {
data: true,
key: true,
},
where: { key: { equals: key } },
})
return doc.data
}
async getKeys(): Promise<string[]> {
const { docs } = await this.payload.db.find<{
key: string
}>({
collection: 'payload-cache',
limit: 0,
pagination: false,
req,
select: { key: true },
})
return docs.map((doc) => doc.key)
}
async set(key: string, data: Data): Promise<void> {
await this.payload.db.updateOne({
collection: 'payload-cache',
data: {
data,
},
joins: false,
req,
select: {},
where: { key: { equals: key } },
})
}
}
constructor.getPayloadCacheCollection = function () {
return {
slug: 'payload-cache',
fields: [
{
name: 'key',
type: 'text',
index: true,
required: true,
unique: true,
},
{
name: 'data',
type: 'json',
required: true,
},
],
...collectionOverrides,
}
}
return constructor
}

38
packages/payload/src/cache/in-memory.ts vendored Normal file
View File

@@ -0,0 +1,38 @@
import type { PayloadCache } from './index.js'
export class InMemoryCache<Data extends Record<string, unknown> = Record<string, unknown>>
implements PayloadCache<Data>
{
store = new Map<string, Data>()
type = 'local'
clear(): void {
this.store.clear()
}
delete(key: string): void {
this.store.delete(key)
}
getAll(): { data: Data; key: string }[] {
const result: { data: Data; key: string }[] = []
this.store.forEach((data, key) => {
result.push({ data, key })
})
return result
}
getByKey(key: string): Data | null {
return this.store.get(key) ?? null
}
getKeys(): string[] {
return Array.from(this.store.keys())
}
set(key: string, data: Data): void {
this.store.set(key, data)
}
}

25
packages/payload/src/cache/index.ts vendored Normal file
View File

@@ -0,0 +1,25 @@
import type { CollectionConfig } from '../collections/config/types.js'
import type { Payload } from '../types/index.js'
export interface PayloadCache<Data extends Record<string, unknown> = Record<string, unknown>> {
clear(): Promise<void> | void
delete(key: string): Promise<void> | void
getAll(): { data: Data; key: string }[] | Promise<{ data: Data; key: string }[]>
getByKey(key: string): Data | null | Promise<Data | null>
getKeys(): Promise<string[]> | string[]
set(key: string, data: Data): Promise<void> | void
type: string
}
export type PayloadCacheConstructor<
Data extends Record<string, unknown> = Record<string, unknown>,
> = {
getPayloadCacheCollection?(): CollectionConfig
new (payload: Payload): PayloadCache<Data>
}

View File

@@ -17,6 +17,7 @@ import { type ClientGlobalConfig, createClientGlobalConfigs } from '../globals/c
export type ServerOnlyRootProperties = keyof Pick<
SanitizedConfig,
| 'bin'
| 'cache'
| 'cors'
| 'csrf'
| 'custom'
@@ -67,6 +68,7 @@ export const serverOnlyConfigProperties: readonly Partial<ServerOnlyRootProperti
'graphQL',
'jobs',
'logger',
'cache',
// `admin`, `onInit`, `localization`, `collections`, and `globals` are all handled separately
]

View File

@@ -2,6 +2,7 @@ import type { JobsConfig } from '../queues/config/types/index.js'
import type { Config } from './types.js'
import defaultAccess from '../auth/defaultAccess.js'
import { InMemoryCache } from '../cache/in-memory.js'
export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
admin: {
@@ -31,6 +32,7 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
theme: 'all',
},
bin: [],
cache: InMemoryCache,
collections: [],
cookiePrefix: 'payload',
cors: [],

View File

@@ -18,6 +18,7 @@ import { sanitizeGlobals } from '../globals/config/sanitize.js'
import { getLockedDocumentsCollection } from '../lockedDocuments/lockedDocumentsCollection.js'
import getPreferencesCollection from '../preferences/preferencesCollection.js'
import { getDefaultJobsCollection } from '../queues/config/jobsCollection.js'
import { emitEventEndpoint, subscribeEndpoint } from '../realtime/index.js'
import checkDuplicateCollections from '../utilities/checkDuplicateCollections.js'
import { defaults } from './defaults.js'
@@ -171,6 +172,13 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
config.i18n = i18nConfig
if (typeof configWithDefaults.cache.getPayloadCacheCollection === 'function') {
configWithDefaults.collections.push(configWithDefaults.cache.getPayloadCacheCollection())
}
configWithDefaults.endpoints.push(subscribeEndpoint)
configWithDefaults.endpoints.push(emitEventEndpoint)
// Need to add default jobs collection before locked documents collections
if (Array.isArray(configWithDefaults.jobs?.tasks) && configWithDefaults.jobs.tasks.length > 0) {
let defaultJobsCollection = getDefaultJobsCollection(config as unknown as Config)

View File

@@ -28,6 +28,7 @@ import type {
Imports,
InternalImportMap,
} from '../bin/generateImportMap/index.js'
import type { PayloadCacheConstructor } from '../cache/index.js'
import type {
Collection,
CollectionConfig,
@@ -38,6 +39,7 @@ import type { EmailAdapter, SendEmailOptions } from '../email/types.js'
import type { ErrorName } from '../errors/types.js'
import type { GlobalConfig, Globals, SanitizedGlobalConfig } from '../globals/config/types.js'
import type { JobsConfig, Payload, RequestContext, TypedUser } from '../index.js'
import type { RealtimeEvent, SanitizedRealtimeConfig } from '../realtime/index.js'
import type { PayloadRequest, Where } from '../types/index.js'
import type { PayloadLogger } from '../utilities/logger.js'
@@ -847,6 +849,8 @@ export type Config = {
}
/** Custom Payload bin scripts can be injected via the config. */
bin?: BinScriptConfig[]
/** Caching adapter, defaults to in-memory */
cache?: PayloadCacheConstructor
/**
* Manage the datamodel of your application
*
@@ -1028,6 +1032,9 @@ export type Config = {
* @see https://payloadcms.com/docs/plugins/overview
*/
plugins?: Plugin[]
realtime?: {
events?: RealtimeEvent[]
}
/** Control the routing structure that Payload binds itself to. */
routes?: {
/** The route for the admin panel.
@@ -1109,6 +1116,7 @@ export type SanitizedConfig = {
configDir: string
rawConfig: string
}
realtime: false | SanitizedRealtimeConfig
upload: {
/**
* Deduped list of adapters used in the project

View File

@@ -19,6 +19,7 @@ import type { Options as VerifyEmailOptions } from './auth/operations/local/veri
import type { Result as LoginResult } from './auth/operations/login.js'
import type { Result as ResetPasswordResult } from './auth/operations/resetPassword.js'
import type { AuthStrategy, User } from './auth/types.js'
import type { PayloadCache } from './cache/index.js'
import type {
BulkOperationResult,
Collection,
@@ -74,6 +75,7 @@ import { consoleEmailAdapter } from './email/consoleEmailAdapter.js'
import { fieldAffectsData } from './fields/config/types.js'
import localGlobalOperations from './globals/operations/local/index.js'
import { getJobsLocalAPI } from './queues/localAPI.js'
import { Realtime } from './realtime/index.js'
import { getLogger } from './utilities/logger.js'
import { serverInit as serverInitTelemetry } from './utilities/telemetry/events/serverInit.js'
import { traverseFields } from './utilities/traverseFields.js'
@@ -242,9 +244,11 @@ export class BasePayload {
authStrategies: AuthStrategy[]
collections: Record<CollectionSlug, Collection> = {}
cache: PayloadCache
collections: Record<CollectionSlug, Collection> = {}
config: SanitizedConfig
/**
* @description Performs count operation
* @param options
@@ -292,8 +296,8 @@ export class BasePayload {
const { create } = localOperations
return create<TSlug, TSelect>(this, options)
}
db: DatabaseAdapter
decrypt = decrypt
duplicate = async <TSlug extends CollectionSlug, TSelect extends SelectFromCollectionSlug<TSlug>>(
@@ -305,11 +309,11 @@ export class BasePayload {
email: InitializedEmailAdapter
encrypt = encrypt
// TODO: re-implement or remove?
// errorHandler: ErrorHandler
encrypt = encrypt
extensions: (args: {
args: OperationArgs<any>
req: graphQLRequest<unknown, unknown>
@@ -425,6 +429,8 @@ export class BasePayload {
return login<TSlug>(this, options)
}
realtime = new Realtime(this)
resetPassword = async <TSlug extends CollectionSlug>(
options: ResetPasswordOptions<TSlug>,
): Promise<ResetPasswordResult> => {
@@ -688,6 +694,8 @@ export class BasePayload {
})
}
this.cache = new this.config.cache(this)
if (!options.disableOnInit) {
if (typeof options.onInit === 'function') {
await options.onInit(this)

View File

@@ -0,0 +1,97 @@
import type { Endpoint } from '../config/types.js'
export type RealtimeEvent = {
data: Record<string, unknown>
name: string
}
export type SanitizedRealtimeConfig = {
events: RealtimeEvent[]
}
export type SubscribeCallback = (args: {
data: Record<string, unknown>
event: string
}) => Promise<void> | void
export class Realtime {
subscribers: SubscribeCallback[] = []
emit({ data, event }: { data: Record<string, unknown>; event: string }): void {
this.subscribers.forEach((callback) => {
void callback({ data, event })
})
}
subscribe(callback: SubscribeCallback): SubscribeCallback {
this.subscribers.push(callback)
return callback
}
unsubscribe(callback: SubscribeCallback): void {
this.subscribers = this.subscribers.filter((each) => each !== callback)
}
}
export const subscribeEndpoint: Endpoint = {
handler: (req) => {
const headers = new Headers()
headers.set('Content-Type', 'text/event-stream')
headers.set('Cache-Control', 'no-cache')
headers.set('Connection', 'keep-alive')
headers.set('Transfer-Encoding', 'chunked')
const abortController = new AbortController()
const stream = new ReadableStream({
cancel() {
// Ensure abort is triggered if the stream is canceled
abortController.abort()
},
start(controller) {
let isClosed = false
const closeStream = () => {
if (!isClosed) {
isClosed = true
req.payload.realtime.unsubscribe(subscription)
try {
controller.close()
} catch {}
}
}
const subscription = req.payload.realtime.subscribe((args) => {
if (isClosed) {
return
} // Prevent enqueueing after close
try {
controller.enqueue(`data: ${JSON.stringify(args)}\n\n`)
} catch {
closeStream()
}
})
// Handle stream cancellation
abortController.signal.addEventListener('abort', closeStream)
},
})
return new Response(stream, { headers })
},
method: 'get',
path: '/realtime/subscribe',
}
export const emitEventEndpoint: Endpoint = {
handler: async (req) => {
const json = await req.json()
req.payload.realtime.emit(json)
return new Response('Event emitted')
},
method: 'post',
path: '/realtime/emit',
}

View File

@@ -0,0 +1,10 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp

24
packages/sdk/.swcrc Normal file
View File

@@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": true,
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": true,
"dts": true
},
"transform": {
"react": {
"runtime": "automatic",
"pragmaFrag": "React.Fragment",
"throwIfNamespace": true,
"development": false,
"useBuiltins": true
}
}
},
"module": {
"type": "es6"
}
}

View File

@@ -0,0 +1,18 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{
languageOptions: {
parserOptions: {
...rootParserOptions,
tsconfigRootDir: import.meta.dirname,
},
},
},
]
export default index

22
packages/sdk/license.md Normal file
View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2018-2024 Payload CMS, Inc. <info@payloadcms.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

63
packages/sdk/package.json Normal file
View File

@@ -0,0 +1,63 @@
{
"name": "@payloadcms/sdk",
"version": "3.1.0",
"description": "The official Payload REST API SDK",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/sdk"
},
"license": "MIT",
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
"maintainers": [
{
"name": "Payload",
"email": "info@payloadcms.com",
"url": "https://payloadcms.com"
}
],
"type": "module",
"exports": {
".": {
"import": "./src/index.ts",
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"main": "./src/index.ts",
"types": "./src/index.ts",
"files": [
"dist"
],
"scripts": {
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf {dist,*.tsbuildinfo}",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"dependencies": {
"payload": "workspace:*",
"qs-esm": "7.0.2",
"ts-essentials": "10.0.3"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*"
},
"publishConfig": {
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"
}
}

View File

@@ -0,0 +1,31 @@
import type { PayloadSDK } from '../index.js'
import type { AuthCollectionSlug, PayloadGeneratedTypes, TypedAuth } from '../types.js'
export type ForgotPasswordOptions<
T extends PayloadGeneratedTypes,
TSlug extends AuthCollectionSlug<T>,
> = {
collection: TSlug
data: {
disableEmail?: boolean
expiration?: number
} & Omit<TypedAuth<T>[TSlug]['forgotPassword'], 'password'>
}
export async function forgotPassword<
T extends PayloadGeneratedTypes,
TSlug extends AuthCollectionSlug<T>,
>(
sdk: PayloadSDK<T>,
options: ForgotPasswordOptions<T, TSlug>,
init?: RequestInit,
): Promise<{ message: string }> {
const response = await sdk.request({
init,
json: options.data,
method: 'POST',
path: `/${options.collection}/forgot-password`,
})
return response.json()
}

View File

@@ -0,0 +1,35 @@
import type { PayloadSDK } from '../index.js'
import type {
AuthCollectionSlug,
DataFromCollectionSlug,
PayloadGeneratedTypes,
TypedAuth,
} from '../types.js'
export type LoginOptions<T extends PayloadGeneratedTypes, TSlug extends AuthCollectionSlug<T>> = {
collection: TSlug
data: TypedAuth<T>[TSlug]['login']
}
export type LoginResult<T extends PayloadGeneratedTypes, TSlug extends AuthCollectionSlug<T>> = {
exp?: number
message: string
token?: string
// @ts-expect-error auth collection and user collection
user: DataFromCollectionSlug<T, TSlug>
}
export async function login<T extends PayloadGeneratedTypes, TSlug extends AuthCollectionSlug<T>>(
sdk: PayloadSDK<T>,
options: LoginOptions<T, TSlug>,
init?: RequestInit,
): Promise<LoginResult<T, TSlug>> {
const response = await sdk.request({
init,
json: options.data,
method: 'POST',
path: `/${options.collection}/login`,
})
return response.json()
}

View File

@@ -0,0 +1,30 @@
import type { PayloadSDK } from '../index.js'
import type { AuthCollectionSlug, DataFromCollectionSlug, PayloadGeneratedTypes } from '../types.js'
export type MeOptions<T extends PayloadGeneratedTypes, TSlug extends AuthCollectionSlug<T>> = {
collection: TSlug
}
export type MeResult<T extends PayloadGeneratedTypes, TSlug extends AuthCollectionSlug<T>> = {
collection?: TSlug
exp?: number
message: string
strategy?: string
token?: string
// @ts-expect-error auth collection and user collection
user: DataFromCollectionSlug<T, TSlug>
}
export async function me<T extends PayloadGeneratedTypes, TSlug extends AuthCollectionSlug<T>>(
sdk: PayloadSDK<T>,
options: MeOptions<T, TSlug>,
init?: RequestInit,
): Promise<MeResult<T, TSlug>> {
const response = await sdk.request({
init,
method: 'GET',
path: `/${options.collection}/me`,
})
return response.json()
}

View File

@@ -0,0 +1,32 @@
import type { PayloadSDK } from '../index.js'
import type { AuthCollectionSlug, DataFromCollectionSlug, PayloadGeneratedTypes } from '../types.js'
export type RefreshOptions<T extends PayloadGeneratedTypes, TSlug extends AuthCollectionSlug<T>> = {
collection: TSlug
}
export type RefreshResult<T extends PayloadGeneratedTypes, TSlug extends AuthCollectionSlug<T>> = {
exp: number
refreshedToken: string
setCookie?: boolean
strategy?: string
// @ts-expect-error auth collection and user collection
user: DataFromCollectionSlug<T, TSlug>
}
export async function refreshToken<
T extends PayloadGeneratedTypes,
TSlug extends AuthCollectionSlug<T>,
>(
sdk: PayloadSDK<T>,
options: RefreshOptions<T, TSlug>,
init?: RequestInit,
): Promise<RefreshResult<T, TSlug>> {
const response = await sdk.request({
init,
method: 'POST',
path: `/${options.collection}/refresh-token`,
})
return response.json()
}

View File

@@ -0,0 +1,40 @@
import type { PayloadSDK } from '../index.js'
import type { AuthCollectionSlug, DataFromCollectionSlug, PayloadGeneratedTypes } from '../types.js'
export type ResetPasswordOptions<
T extends PayloadGeneratedTypes,
TSlug extends AuthCollectionSlug<T>,
> = {
collection: TSlug
data: {
password: string
token: string
}
}
export type ResetPasswordResult<
T extends PayloadGeneratedTypes,
TSlug extends AuthCollectionSlug<T>,
> = {
token?: string
// @ts-expect-error auth collection and user collection
user: DataFromCollectionSlug<T, TSlug>
}
export async function resetPassword<
T extends PayloadGeneratedTypes,
TSlug extends AuthCollectionSlug<T>,
>(
sdk: PayloadSDK<T>,
options: ResetPasswordOptions<T, TSlug>,
init?: RequestInit,
): Promise<ResetPasswordResult<T, TSlug>> {
const response = await sdk.request({
init,
json: options.data,
method: 'POST',
path: `/${options.collection}/reset-password`,
})
return response.json()
}

View File

@@ -0,0 +1,27 @@
import type { PayloadSDK } from '../index.js'
import type { AuthCollectionSlug, PayloadGeneratedTypes } from '../types.js'
export type VerifyEmailOptions<
T extends PayloadGeneratedTypes,
TSlug extends AuthCollectionSlug<T>,
> = {
collection: TSlug
token: string
}
export async function verifyEmail<
T extends PayloadGeneratedTypes,
TSlug extends AuthCollectionSlug<T>,
>(
sdk: PayloadSDK<T>,
options: VerifyEmailOptions<T, TSlug>,
init?: RequestInit,
): Promise<{ message: string }> {
const response = await sdk.request({
init,
method: 'POST',
path: `/${options.collection}/verify/${options.token}`,
})
return response.json()
}

View File

@@ -0,0 +1,25 @@
import type { Where } from 'payload'
import type { PayloadSDK } from '../index.js'
import type { CollectionSlug, PayloadGeneratedTypes, TypedLocale } from '../types.js'
export type CountOptions<T extends PayloadGeneratedTypes, TSlug extends CollectionSlug<T>> = {
collection: TSlug
locale?: 'all' | TypedLocale<T>
where?: Where
}
export async function count<T extends PayloadGeneratedTypes, TSlug extends CollectionSlug<T>>(
sdk: PayloadSDK<T>,
options: CountOptions<T, TSlug>,
init?: RequestInit,
): Promise<{ totalDocs: number }> {
const response = await sdk.request({
args: options,
init,
method: 'GET',
path: `/${options.collection}/count`,
})
return response.json()
}

View File

@@ -0,0 +1,60 @@
import type { SelectType } from 'payload'
import type { PayloadSDK } from '../index.js'
import type {
CollectionSlug,
PayloadGeneratedTypes,
PopulateType,
RequiredDataFromCollectionSlug,
TransformCollectionWithSelect,
TypedLocale,
UploadCollectionSlug,
} from '../types.js'
import { resolveFileFromOptions } from '../utilities/resolveFileFromOptions.js'
export type CreateOptions<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TSelect extends SelectType,
> = {
collection: TSlug
data: RequiredDataFromCollectionSlug<T, TSlug>
depth?: number
draft?: boolean
fallbackLocale?: false | TypedLocale<T>
/** File Blob object or URL to the file. Only for upload collections */
file?: TSlug extends UploadCollectionSlug<T> ? Blob | string : never
locale?: 'all' | TypedLocale<T>
populate?: PopulateType<T>
select?: TSelect
}
export async function create<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TSelect extends SelectType,
>(
sdk: PayloadSDK<T>,
options: CreateOptions<T, TSlug, TSelect>,
init?: RequestInit,
): Promise<TransformCollectionWithSelect<T, TSlug, TSelect>> {
let file: Blob | undefined = undefined
if (options.file) {
file = await resolveFileFromOptions(options.file)
}
const response = await sdk.request({
args: options,
file,
init,
json: options.data,
method: 'POST',
path: `/${options.collection}`,
})
const json = await response.json()
return json.doc
}

View File

@@ -0,0 +1,77 @@
import type { SelectType, Where } from 'payload'
import type { PayloadSDK } from '../index.js'
import type {
BulkOperationResult,
CollectionSlug,
PayloadGeneratedTypes,
PopulateType,
SelectFromCollectionSlug,
TransformCollectionWithSelect,
TypedLocale,
} from '../types.js'
export type DeleteBaseOptions<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TSelect extends SelectType,
> = {
collection: TSlug
depth?: number
draft?: boolean
fallbackLocale?: false | TypedLocale<T>
locale?: TypedLocale<T>
populate?: PopulateType<T>
select?: TSelect
}
export type DeleteByIDOptions<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TSelect extends SelectFromCollectionSlug<T, TSlug>,
> = {
id: number | string
where?: never
} & DeleteBaseOptions<T, TSlug, TSelect>
export type DeleteManyOptions<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TSelect extends SelectFromCollectionSlug<T, TSlug>,
> = {
id?: never
where: Where
} & DeleteBaseOptions<T, TSlug, TSelect>
export type DeleteOptions<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TSelect extends SelectFromCollectionSlug<T, TSlug>,
> = DeleteByIDOptions<T, TSlug, TSelect> | DeleteManyOptions<T, TSlug, TSelect>
export async function deleteOperation<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TSelect extends SelectFromCollectionSlug<T, TSlug>,
>(
sdk: PayloadSDK<T>,
options: DeleteOptions<T, TSlug, TSelect>,
init?: RequestInit,
): Promise<
BulkOperationResult<T, TSlug, TSelect> | TransformCollectionWithSelect<T, TSlug, TSelect>
> {
const response = await sdk.request({
args: options,
init,
method: 'DELETE',
path: `/${options.collection}${options.id ? `/${options.id}` : ''}`,
})
const json = await response.json()
if (options.id) {
return json.doc
}
return json
}

View File

@@ -0,0 +1,49 @@
import type { PaginatedDocs, SelectType, Sort, Where } from 'payload'
import type { PayloadSDK } from '../index.js'
import type {
CollectionSlug,
JoinQuery,
PayloadGeneratedTypes,
PopulateType,
TransformCollectionWithSelect,
TypedLocale,
} from '../types.js'
export type FindOptions<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TSelect extends SelectType,
> = {
collection: TSlug
depth?: number
draft?: boolean
fallbackLocale?: false | TypedLocale<T>
joins?: JoinQuery<T, TSlug>
limit?: number
locale?: 'all' | TypedLocale<T>
page?: number
populate?: PopulateType<T>
select?: TSelect
sort?: Sort
where?: Where
}
export async function find<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TSelect extends SelectType,
>(
sdk: PayloadSDK<T>,
options: FindOptions<T, TSlug, TSelect>,
init?: RequestInit,
): Promise<PaginatedDocs<TransformCollectionWithSelect<T, TSlug, TSelect>>> {
const response = await sdk.request({
args: options,
init,
method: 'GET',
path: `/${options.collection}`,
})
return response.json()
}

View File

@@ -0,0 +1,62 @@
import type { ApplyDisableErrors, SelectType } from 'payload'
import type { PayloadSDK } from '../index.js'
import type {
CollectionSlug,
JoinQuery,
PayloadGeneratedTypes,
PopulateType,
TransformCollectionWithSelect,
TypedLocale,
} from '../types.js'
export type FindByIDOptions<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TDisableErrors extends boolean,
TSelect extends SelectType,
> = {
collection: TSlug
depth?: number
disableErrors?: TDisableErrors
draft?: boolean
fallbackLocale?: false | TypedLocale<T>
id: number | string
joins?: JoinQuery<T, TSlug>
locale?: 'all' | TypedLocale<T>
populate?: PopulateType<T>
select?: TSelect
}
export async function findByID<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TDisableErrors extends boolean,
TSelect extends SelectType,
>(
sdk: PayloadSDK<T>,
options: FindByIDOptions<T, TSlug, TDisableErrors, TSelect>,
init?: RequestInit,
): Promise<ApplyDisableErrors<TransformCollectionWithSelect<T, TSlug, TSelect>, TDisableErrors>> {
try {
const response = await sdk.request({
args: options,
init,
method: 'GET',
path: `/${options.collection}/${options.id}`,
})
if (response.ok) {
return response.json()
} else {
throw new Error()
}
} catch {
if (options.disableErrors) {
// @ts-expect-error generic nullable
return null
}
throw new Error(`Error retrieving the document ${options.collection}/${options.id}`)
}
}

View File

@@ -0,0 +1,58 @@
import type { ApplyDisableErrors, SelectType, TypeWithVersion } from 'payload'
import type { PayloadSDK } from '../index.js'
import type {
CollectionSlug,
DataFromCollectionSlug,
PayloadGeneratedTypes,
PopulateType,
TypedLocale,
} from '../types.js'
export type FindVersionByIDOptions<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TDisableErrors extends boolean,
> = {
collection: TSlug
depth?: number
disableErrors?: TDisableErrors
draft?: boolean
fallbackLocale?: false | TypedLocale<T>
id: number | string
locale?: 'all' | TypedLocale<T>
populate?: PopulateType<T>
select?: SelectType
}
export async function findVersionByID<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TDisableErrors extends boolean,
>(
sdk: PayloadSDK<T>,
options: FindVersionByIDOptions<T, TSlug, TDisableErrors>,
init?: RequestInit,
): Promise<ApplyDisableErrors<TypeWithVersion<DataFromCollectionSlug<T, TSlug>>, TDisableErrors>> {
try {
const response = await sdk.request({
args: options,
init,
method: 'GET',
path: `/${options.collection}/versions/${options.id}`,
})
if (response.ok) {
return response.json()
} else {
throw new Error()
}
} catch {
if (options.disableErrors) {
// @ts-expect-error generic nullable
return null
}
throw new Error(`Error retrieving the version document ${options.collection}/${options.id}`)
}
}

View File

@@ -0,0 +1,45 @@
import type { PaginatedDocs, SelectType, Sort, TypeWithVersion, Where } from 'payload'
import type { PayloadSDK } from '../index.js'
import type {
CollectionSlug,
DataFromCollectionSlug,
PayloadGeneratedTypes,
PopulateType,
TypedLocale,
} from '../types.js'
export type FindVersionsOptions<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
> = {
collection: TSlug
depth?: number
draft?: boolean
fallbackLocale?: false | TypedLocale<T>
limit?: number
locale?: 'all' | TypedLocale<T>
page?: number
populate?: PopulateType<T>
select?: SelectType
sort?: Sort
where?: Where
}
export async function findVersions<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
>(
sdk: PayloadSDK<T>,
options: FindVersionsOptions<T, TSlug>,
init?: RequestInit,
): Promise<PaginatedDocs<TypeWithVersion<DataFromCollectionSlug<T, TSlug>>>> {
const response = await sdk.request({
args: options,
init,
method: 'GET',
path: `/${options.collection}/versions`,
})
return response.json()
}

View File

@@ -0,0 +1,39 @@
import type { PayloadSDK } from '../index.js'
import type {
CollectionSlug,
DataFromCollectionSlug,
PayloadGeneratedTypes,
PopulateType,
TypedLocale,
} from '../types.js'
export type RestoreVersionByIDOptions<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
> = {
collection: TSlug
depth?: number
draft?: boolean
fallbackLocale?: false | TypedLocale<T>
id: number | string
locale?: 'all' | TypedLocale<T>
populate?: PopulateType<T>
}
export async function restoreVersion<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
>(
sdk: PayloadSDK<T>,
options: RestoreVersionByIDOptions<T, TSlug>,
init?: RequestInit,
): Promise<DataFromCollectionSlug<T, TSlug>> {
const response = await sdk.request({
args: options,
init,
method: 'POST',
path: `/${options.collection}/versions/${options.id}`,
})
return response.json()
}

View File

@@ -0,0 +1,96 @@
import type { SelectType, Where } from 'payload'
import type { DeepPartial } from 'ts-essentials'
import type { PayloadSDK } from '../index.js'
import type {
BulkOperationResult,
CollectionSlug,
PayloadGeneratedTypes,
PopulateType,
RequiredDataFromCollectionSlug,
SelectFromCollectionSlug,
TransformCollectionWithSelect,
TypedLocale,
} from '../types.js'
import { resolveFileFromOptions } from '../utilities/resolveFileFromOptions.js'
export type UpdateBaseOptions<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TSelect extends SelectType,
> = {
autosave?: boolean
collection: TSlug
data: DeepPartial<RequiredDataFromCollectionSlug<T, TSlug>>
depth?: number
draft?: boolean
fallbackLocale?: false | TypedLocale<T>
file?: File | string
filePath?: string
locale?: TypedLocale<T>
populate?: PopulateType<T>
publishSpecificLocale?: string
select?: TSelect
}
export type UpdateByIDOptions<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TSelect extends SelectFromCollectionSlug<T, TSlug>,
> = {
id: number | string
limit?: never
where?: never
} & UpdateBaseOptions<T, TSlug, TSelect>
export type UpdateManyOptions<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TSelect extends SelectFromCollectionSlug<T, TSlug>,
> = {
id?: never
limit?: number
where: Where
} & UpdateBaseOptions<T, TSlug, TSelect>
export type UpdateOptions<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TSelect extends SelectFromCollectionSlug<T, TSlug>,
> = UpdateByIDOptions<T, TSlug, TSelect> | UpdateManyOptions<T, TSlug, TSelect>
export async function update<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TSelect extends SelectFromCollectionSlug<T, TSlug>,
>(
sdk: PayloadSDK<T>,
options: UpdateOptions<T, TSlug, TSelect>,
init?: RequestInit,
): Promise<
BulkOperationResult<T, TSlug, TSelect> | TransformCollectionWithSelect<T, TSlug, TSelect>
> {
let file: Blob | undefined = undefined
if (options.file) {
file = await resolveFileFromOptions(options.file)
}
const response = await sdk.request({
args: options,
file,
init,
json: options.data,
method: 'PATCH',
path: `/${options.collection}${options.id ? `/${options.id}` : ''}`,
})
const json = await response.json()
if (options.id) {
return json.doc
}
return json
}

View File

@@ -0,0 +1,44 @@
import type { SelectType } from 'payload'
import type { PayloadSDK } from '../index.js'
import type {
GlobalSlug,
PayloadGeneratedTypes,
PopulateType,
SelectFromGlobalSlug,
TransformGlobalWithSelect,
TypedLocale,
} from '../types.js'
export type FindGlobalOptions<
T extends PayloadGeneratedTypes,
TSlug extends GlobalSlug<T>,
TSelect extends SelectType,
> = {
depth?: number
draft?: boolean
fallbackLocale?: false | TypedLocale<T>
locale?: 'all' | TypedLocale<T>
populate?: PopulateType<T>
select?: TSelect
slug: TSlug
}
export async function findGlobal<
T extends PayloadGeneratedTypes,
TSlug extends GlobalSlug<T>,
TSelect extends SelectFromGlobalSlug<T, TSlug>,
>(
sdk: PayloadSDK<T>,
options: FindGlobalOptions<T, TSlug, TSelect>,
init?: RequestInit,
): Promise<TransformGlobalWithSelect<T, TSlug, TSelect>> {
const response = await sdk.request({
args: options,
init,
method: 'GET',
path: `/globals/${options.slug}`,
})
return response.json()
}

View File

@@ -0,0 +1,58 @@
import type { ApplyDisableErrors, SelectType, TypeWithVersion } from 'payload'
import type { PayloadSDK } from '../index.js'
import type {
DataFromGlobalSlug,
GlobalSlug,
PayloadGeneratedTypes,
PopulateType,
TypedLocale,
} from '../types.js'
export type FindGlobalVersionByIDOptions<
T extends PayloadGeneratedTypes,
TSlug extends GlobalSlug<T>,
TDisableErrors extends boolean,
> = {
depth?: number
disableErrors?: TDisableErrors
draft?: boolean
fallbackLocale?: false | TypedLocale<T>
id: number | string
locale?: 'all' | TypedLocale<T>
populate?: PopulateType<T>
select?: SelectType
slug: TSlug
}
export async function findGlobalVersionByID<
T extends PayloadGeneratedTypes,
TSlug extends GlobalSlug<T>,
TDisableErrors extends boolean,
>(
sdk: PayloadSDK<T>,
options: FindGlobalVersionByIDOptions<T, TSlug, TDisableErrors>,
init?: RequestInit,
): Promise<ApplyDisableErrors<TypeWithVersion<DataFromGlobalSlug<T, TSlug>>, TDisableErrors>> {
try {
const response = await sdk.request({
args: options,
init,
method: 'GET',
path: `/globals/${options.slug}/versions/${options.id}`,
})
if (response.ok) {
return response.json()
} else {
throw new Error()
}
} catch {
if (options.disableErrors) {
// @ts-expect-error generic nullable
return null
}
throw new Error(`Error retrieving the version document ${options.slug}/${options.id}`)
}
}

View File

@@ -0,0 +1,45 @@
import type { PaginatedDocs, SelectType, Sort, TypeWithVersion, Where } from 'payload'
import type { PayloadSDK } from '../index.js'
import type {
DataFromGlobalSlug,
GlobalSlug,
PayloadGeneratedTypes,
PopulateType,
TypedLocale,
} from '../types.js'
export type FindGlobalVersionsOptions<
T extends PayloadGeneratedTypes,
TSlug extends GlobalSlug<T>,
> = {
depth?: number
draft?: boolean
fallbackLocale?: false | TypedLocale<T>
limit?: number
locale?: 'all' | TypedLocale<T>
page?: number
populate?: PopulateType<T>
select?: SelectType
slug: TSlug
sort?: Sort
where?: Where
}
export async function findGlobalVersions<
T extends PayloadGeneratedTypes,
TSlug extends GlobalSlug<T>,
>(
sdk: PayloadSDK<T>,
options: FindGlobalVersionsOptions<T, TSlug>,
init?: RequestInit,
): Promise<PaginatedDocs<TypeWithVersion<DataFromGlobalSlug<T, TSlug>>>> {
const response = await sdk.request({
args: options,
init,
method: 'GET',
path: `/globals/${options.slug}/versions`,
})
return response.json()
}

View File

@@ -0,0 +1,43 @@
import type { TypeWithVersion } from 'payload'
import type { PayloadSDK } from '../index.js'
import type {
DataFromGlobalSlug,
GlobalSlug,
PayloadGeneratedTypes,
PopulateType,
TypedLocale,
} from '../types.js'
export type RestoreGlobalVersionByIDOptions<
T extends PayloadGeneratedTypes,
TSlug extends GlobalSlug<T>,
> = {
depth?: number
draft?: boolean
fallbackLocale?: false | TypedLocale<T>
id: number | string
locale?: 'all' | TypedLocale<T>
populate?: PopulateType<T>
slug: TSlug
}
export async function restoreGlobalVersion<
T extends PayloadGeneratedTypes,
TSlug extends GlobalSlug<T>,
>(
sdk: PayloadSDK<T>,
options: RestoreGlobalVersionByIDOptions<T, TSlug>,
init?: RequestInit,
): Promise<TypeWithVersion<DataFromGlobalSlug<T, TSlug>>> {
const response = await sdk.request({
args: options,
init,
method: 'POST',
path: `/globals/${options.slug}/versions/${options.id}`,
})
const { doc } = await response.json()
return doc
}

View File

@@ -0,0 +1,51 @@
import type { SelectType } from 'payload'
import type { DeepPartial } from 'ts-essentials'
import type { PayloadSDK } from '../index.js'
import type {
DataFromGlobalSlug,
GlobalSlug,
PayloadGeneratedTypes,
PopulateType,
SelectFromGlobalSlug,
TransformGlobalWithSelect,
TypedLocale,
} from '../types.js'
export type UpdateGlobalOptions<
T extends PayloadGeneratedTypes,
TSlug extends GlobalSlug<T>,
TSelect extends SelectType,
> = {
data: DeepPartial<Omit<DataFromGlobalSlug<T, TSlug>, 'id'>>
depth?: number
draft?: boolean
fallbackLocale?: false | TypedLocale<T>
locale?: 'all' | TypedLocale<T>
populate?: PopulateType<T>
publishSpecificLocale?: TypedLocale<T>
select?: TSelect
slug: TSlug
}
export async function updateGlobal<
T extends PayloadGeneratedTypes,
TSlug extends GlobalSlug<T>,
TSelect extends SelectFromGlobalSlug<T, TSlug>,
>(
sdk: PayloadSDK<T>,
options: UpdateGlobalOptions<T, TSlug, TSelect>,
init?: RequestInit,
): Promise<TransformGlobalWithSelect<T, TSlug, TSelect>> {
const response = await sdk.request({
args: options,
init,
json: options.data,
method: 'POST',
path: `/globals/${options.slug}`,
})
const { result } = await response.json()
return result
}

385
packages/sdk/src/index.ts Normal file
View File

@@ -0,0 +1,385 @@
import type { ApplyDisableErrors, PaginatedDocs, SelectType, TypeWithVersion } from 'payload'
import type { ForgotPasswordOptions } from './auth/forgotPassword.js'
import type { LoginOptions, LoginResult } from './auth/login.js'
import type { MeOptions, MeResult } from './auth/me.js'
import type { ResetPasswordOptions, ResetPasswordResult } from './auth/resetPassword.js'
import type { CountOptions } from './collections/count.js'
import type { CreateOptions } from './collections/create.js'
import type { DeleteByIDOptions, DeleteManyOptions, DeleteOptions } from './collections/delete.js'
import type { FindOptions } from './collections/find.js'
import type { FindByIDOptions } from './collections/findByID.js'
import type { FindVersionByIDOptions } from './collections/findVersionByID.js'
import type { FindVersionsOptions } from './collections/findVersions.js'
import type { RestoreVersionByIDOptions } from './collections/restoreVersion.js'
import type { FindGlobalVersionByIDOptions } from './globals/findVersionByID.js'
import type { FindGlobalVersionsOptions } from './globals/findVersions.js'
import type { RestoreGlobalVersionByIDOptions } from './globals/restoreVersion.js'
import type { UpdateGlobalOptions } from './globals/update.js'
import type {
AuthCollectionSlug,
BulkOperationResult,
CollectionSlug,
DataFromCollectionSlug,
DataFromGlobalSlug,
GlobalSlug,
PayloadGeneratedTypes,
SelectFromCollectionSlug,
SelectFromGlobalSlug,
TransformCollectionWithSelect,
TransformGlobalWithSelect,
} from './types.js'
import type { OperationArgs } from './utilities/buildSearchParams.js'
import { forgotPassword } from './auth/forgotPassword.js'
import { login } from './auth/login.js'
import { me } from './auth/me.js'
import { type RefreshOptions, type RefreshResult, refreshToken } from './auth/refreshToken.js'
import { resetPassword } from './auth/resetPassword.js'
import { verifyEmail, type VerifyEmailOptions } from './auth/verifyEmail.js'
import { count } from './collections/count.js'
import { create } from './collections/create.js'
import { deleteOperation } from './collections/delete.js'
import { find } from './collections/find.js'
import { findByID } from './collections/findByID.js'
import { findVersionByID } from './collections/findVersionByID.js'
import { findVersions } from './collections/findVersions.js'
import { restoreVersion } from './collections/restoreVersion.js'
import {
update,
type UpdateByIDOptions,
type UpdateManyOptions,
type UpdateOptions,
} from './collections/update.js'
import { findGlobal, type FindGlobalOptions } from './globals/findOne.js'
import { findGlobalVersionByID } from './globals/findVersionByID.js'
import { findGlobalVersions } from './globals/findVersions.js'
import { restoreGlobalVersion } from './globals/restoreVersion.js'
import { updateGlobal } from './globals/update.js'
import { emitEvent } from './realtime/emitEvent.js'
import { subscribe } from './realtime/subscribe.js'
import { buildSearchParams } from './utilities/buildSearchParams.js'
type Args = {
/** Base passed `RequestInit` to `fetch`. For base headers / credentials include etc. */
baseInit?: RequestInit
/**
* Base API URL for requests.
* @example 'https://example.com/api'
*/
baseURL: string
/**
* This option allows you to pass a custom `fetch` implementation.
* The function always receives `path` as the first parameter and `RequestInit` as the second.
* @example For testing without needing an HTTP server:
* ```typescript
* import type { GeneratedTypes, SanitizedConfig } from 'payload';
* import config from '@payload-config';
* import { REST_DELETE, REST_GET, REST_PATCH, REST_POST, REST_PUT } from '@payloadcms/next/routes';
* import { PayloadSDK } from '@payloadcms/sdk';
*
* export type TypedPayloadSDK = PayloadSDK<GeneratedTypes>;
*
* const api = {
* GET: REST_GET(config),
* POST: REST_POST(config),
* PATCH: REST_PATCH(config),
* DELETE: REST_DELETE(config),
* PUT: REST_PUT(config),
* };
*
* const awaitedConfig = await config;
*
* export const sdk = new PayloadSDK<GeneratedTypes>({
* baseURL: '',
* fetch: (path: string, init: RequestInit) => {
* const [slugs, search] = path.slice(1).split('?');
* const url = `${awaitedConfig.serverURL || 'http://localhost:3000'}${awaitedConfig.routes.api}/${slugs}${search ? `?${search}` : ''}`;
*
* if (init.body instanceof FormData) {
* const file = init.body.get('file') as Blob;
* if (file && init.headers instanceof Headers) {
* init.headers.set('Content-Length', file.size.toString());
* }
* }
*
* const request = new Request(url, init);
*
* const params = {
* params: Promise.resolve({
* slug: slugs.split('/'),
* }),
* };
*
* return api[init.method.toUpperCase()](request, params);
* },
* });
* ```
*/
fetch?: typeof fetch
}
export class PayloadSDK<T extends PayloadGeneratedTypes = PayloadGeneratedTypes> {
baseInit: RequestInit
baseURL: string
fetch: typeof fetch
realtime = {
emitEvent: (args: { data: Record<string, any>; event: string }): Promise<Response> => {
return emitEvent(this, args)
},
subscribe: (
onEvent: (args: { data: Record<string, any>; event: string }) => void,
): Promise<{ response: Response; unsubscribe: () => Promise<void> }> => {
return subscribe(this, onEvent)
},
}
constructor(args: Args) {
this.baseURL = args.baseURL
this.fetch = args.fetch ?? ((...args) => globalThis.fetch(...args))
this.baseInit = args.baseInit ?? {}
}
/**
* @description Performs count operation
* @param options
* @returns count of documents satisfying query
*/
count<TSlug extends CollectionSlug<T>>(
options: CountOptions<T, TSlug>,
init?: RequestInit,
): Promise<{ totalDocs: number }> {
return count(this, options, init)
}
/**
* @description Performs create operation
* @param options
* @returns created document
*/
create<TSlug extends CollectionSlug<T>, TSelect extends SelectType>(
options: CreateOptions<T, TSlug, TSelect>,
init?: RequestInit,
): Promise<TransformCollectionWithSelect<T, TSlug, TSelect>> {
return create(this, options, init)
}
delete<TSlug extends CollectionSlug<T>, TSelect extends SelectFromCollectionSlug<T, TSlug>>(
options: DeleteManyOptions<T, TSlug, TSelect>,
init?: RequestInit,
): Promise<BulkOperationResult<T, TSlug, TSelect>>
delete<TSlug extends CollectionSlug<T>, TSelect extends SelectFromCollectionSlug<T, TSlug>>(
options: DeleteByIDOptions<T, TSlug, TSelect>,
init?: RequestInit,
): Promise<TransformCollectionWithSelect<T, TSlug, TSelect>>
/**
* @description Update one or more documents
* @param options
* @returns Updated document(s)
*/
delete<TSlug extends CollectionSlug<T>, TSelect extends SelectFromCollectionSlug<T, TSlug>>(
options: DeleteOptions<T, TSlug, TSelect>,
init?: RequestInit,
): Promise<
BulkOperationResult<T, TSlug, TSelect> | TransformCollectionWithSelect<T, TSlug, TSelect>
> {
return deleteOperation(this, options, init)
}
/**
* @description Find documents with criteria
* @param options
* @returns documents satisfying query
*/
find<TSlug extends CollectionSlug<T>, TSelect extends SelectType>(
options: FindOptions<T, TSlug, TSelect>,
init?: RequestInit,
): Promise<PaginatedDocs<TransformCollectionWithSelect<T, TSlug, TSelect>>> {
return find(this, options, init)
}
/**
* @description Find document by ID
* @param options
* @returns document with specified ID
*/
findByID<
TSlug extends CollectionSlug<T>,
TDisableErrors extends boolean,
TSelect extends SelectType,
>(
options: FindByIDOptions<T, TSlug, TDisableErrors, TSelect>,
init?: RequestInit,
): Promise<ApplyDisableErrors<TransformCollectionWithSelect<T, TSlug, TSelect>, TDisableErrors>> {
return findByID(this, options, init)
}
findGlobal<TSlug extends GlobalSlug<T>, TSelect extends SelectFromGlobalSlug<T, TSlug>>(
options: FindGlobalOptions<T, TSlug, TSelect>,
init?: RequestInit,
): Promise<TransformGlobalWithSelect<T, TSlug, TSelect>> {
return findGlobal(this, options, init)
}
findGlobalVersionByID<TSlug extends GlobalSlug<T>, TDisableErrors extends boolean>(
options: FindGlobalVersionByIDOptions<T, TSlug, TDisableErrors>,
init?: RequestInit,
): Promise<ApplyDisableErrors<TypeWithVersion<DataFromGlobalSlug<T, TSlug>>, TDisableErrors>> {
return findGlobalVersionByID(this, options, init)
}
findGlobalVersions<TSlug extends GlobalSlug<T>>(
options: FindGlobalVersionsOptions<T, TSlug>,
init?: RequestInit,
): Promise<PaginatedDocs<TypeWithVersion<DataFromGlobalSlug<T, TSlug>>>> {
return findGlobalVersions(this, options, init)
}
findVersionByID<TSlug extends CollectionSlug<T>, TDisableErrors extends boolean>(
options: FindVersionByIDOptions<T, TSlug, TDisableErrors>,
init?: RequestInit,
): Promise<
ApplyDisableErrors<TypeWithVersion<DataFromCollectionSlug<T, TSlug>>, TDisableErrors>
> {
return findVersionByID(this, options, init)
}
findVersions<TSlug extends CollectionSlug<T>>(
options: FindVersionsOptions<T, TSlug>,
init?: RequestInit,
): Promise<PaginatedDocs<TypeWithVersion<DataFromCollectionSlug<T, TSlug>>>> {
return findVersions(this, options, init)
}
forgotPassword<TSlug extends AuthCollectionSlug<T>>(
options: ForgotPasswordOptions<T, TSlug>,
init?: RequestInit,
): Promise<{ message: string }> {
return forgotPassword(this, options, init)
}
login<TSlug extends AuthCollectionSlug<T>>(
options: LoginOptions<T, TSlug>,
init?: RequestInit,
): Promise<LoginResult<T, TSlug>> {
return login(this, options, init)
}
me<TSlug extends AuthCollectionSlug<T>>(
options: MeOptions<T, TSlug>,
init?: RequestInit,
): Promise<MeResult<T, TSlug>> {
return me(this, options, init)
}
refreshToken<TSlug extends AuthCollectionSlug<T>>(
options: RefreshOptions<T, TSlug>,
init?: RequestInit,
): Promise<RefreshResult<T, TSlug>> {
return refreshToken(this, options, init)
}
async request({
args = {},
file,
init: incomingInit,
json,
method,
path,
}: {
args?: OperationArgs
file?: Blob
init?: RequestInit
json?: unknown
method: 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT'
path: string
}): Promise<Response> {
const headers = new Headers({ ...this.baseInit.headers, ...incomingInit?.headers })
const init: RequestInit = {
method,
...this.baseInit,
...incomingInit,
headers,
}
if (json) {
if (file) {
const formData = new FormData()
formData.append('file', file)
formData.append('_payload', JSON.stringify(json))
init.body = formData
} else {
headers.set('Content-Type', 'application/json')
init.body = JSON.stringify(json)
}
}
const response = await this.fetch(`${this.baseURL}${path}${buildSearchParams(args)}`, init)
return response
}
resetPassword<TSlug extends AuthCollectionSlug<T>>(
options: ResetPasswordOptions<T, TSlug>,
init?: RequestInit,
): Promise<ResetPasswordResult<T, TSlug>> {
return resetPassword(this, options, init)
}
restoreGlobalVersion<TSlug extends GlobalSlug<T>>(
options: RestoreGlobalVersionByIDOptions<T, TSlug>,
init?: RequestInit,
): Promise<TypeWithVersion<DataFromGlobalSlug<T, TSlug>>> {
return restoreGlobalVersion(this, options, init)
}
restoreVersion<TSlug extends CollectionSlug<T>>(
options: RestoreVersionByIDOptions<T, TSlug>,
init?: RequestInit,
): Promise<DataFromCollectionSlug<T, TSlug>> {
return restoreVersion(this, options, init)
}
update<TSlug extends CollectionSlug<T>, TSelect extends SelectFromCollectionSlug<T, TSlug>>(
options: UpdateManyOptions<T, TSlug, TSelect>,
init?: RequestInit,
): Promise<BulkOperationResult<T, TSlug, TSelect>>
update<TSlug extends CollectionSlug<T>, TSelect extends SelectFromCollectionSlug<T, TSlug>>(
options: UpdateByIDOptions<T, TSlug, TSelect>,
init?: RequestInit,
): Promise<TransformCollectionWithSelect<T, TSlug, TSelect>>
/**
* @description Update one or more documents
* @param options
* @returns Updated document(s)
*/
update<TSlug extends CollectionSlug<T>, TSelect extends SelectFromCollectionSlug<T, TSlug>>(
options: UpdateOptions<T, TSlug, TSelect>,
init?: RequestInit,
): Promise<
BulkOperationResult<T, TSlug, TSelect> | TransformCollectionWithSelect<T, TSlug, TSelect>
> {
return update(this, options, init)
}
updateGlobal<TSlug extends GlobalSlug<T>, TSelect extends SelectFromGlobalSlug<T, TSlug>>(
options: UpdateGlobalOptions<T, TSlug, TSelect>,
init?: RequestInit,
): Promise<TransformGlobalWithSelect<T, TSlug, TSelect>> {
return updateGlobal(this, options, init)
}
verifyEmail<TSlug extends AuthCollectionSlug<T>>(
options: VerifyEmailOptions<T, TSlug>,
init?: RequestInit,
): Promise<{ message: string }> {
return verifyEmail(this, options, init)
}
}

View File

@@ -0,0 +1,18 @@
import type { PayloadSDK } from '../index.js'
import type { PayloadGeneratedTypes } from '../types.js'
export const emitEvent = async <T extends PayloadGeneratedTypes>(
sdk: PayloadSDK<T>,
args: {
data: Record<string, any>
event: string
},
): Promise<Response> => {
const response = await sdk.request({
json: args,
method: 'POST',
path: '/realtime/emit',
})
return response
}

View File

@@ -0,0 +1,65 @@
import type { PayloadSDK } from '../index.js'
import type { PayloadGeneratedTypes } from '../types.js'
export const subscribe = async <T extends PayloadGeneratedTypes>(
sdk: PayloadSDK<T>,
onEvent: (args: { data: Record<string, any>; event: string }) => void,
): Promise<{ response: Response; unsubscribe: () => Promise<void> }> => {
const response = await sdk.request({
method: 'GET',
path: '/realtime/subscribe',
})
if (
!response.ok ||
!response.headers.get('Content-Type')?.includes('text/event-stream') ||
!response.body
) {
throw new Error('Expected SSE response, but got: ' + response.statusText)
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let done = false
let buffer = ''
const unsubscribe = async () => {
// Cancel the stream
done = true
await reader.cancel()
}
const processStream = async () => {
try {
while (!done) {
const { done: doneReading, value } = await reader.read()
if (doneReading) {
break
}
const chunk = decoder.decode(value, { stream: true })
buffer += chunk
let boundary = buffer.indexOf('\n\n')
while (boundary !== -1) {
const eventRaw = buffer.slice(0, boundary).trim()
buffer = buffer.slice(boundary + 2)
const data = JSON.parse(eventRaw.slice(5, boundary))
onEvent(data)
boundary = buffer.indexOf('\n\n')
}
}
} catch (err) {
if (!done) {
console.error('Error processing the stream:', err)
}
} finally {
reader.releaseLock()
}
}
void processStream()
return { response, unsubscribe }
}

169
packages/sdk/src/types.ts Normal file
View File

@@ -0,0 +1,169 @@
import type {
JsonObject,
SelectType,
StringKeyOf,
TransformDataWithSelect,
TypeWithID,
Where,
} from 'payload'
import type { MarkOptional, NonNever } from 'ts-essentials'
export interface PayloadGeneratedTypes {
auth: {
[slug: string]: {
forgotPassword: {
email: string
}
login: {
email: string
password: string
}
registerFirstUser: {
email: string
password: string
}
unlock: {
email: string
}
}
}
collections: {
[slug: string]: JsonObject & TypeWithID
}
collectionsJoins: {
[slug: string]: {
[schemaPath: string]: string
}
}
collectionsSelect: {
[slug: string]: any
}
db: {
defaultIDType: number | string
}
globals: {
[slug: string]: JsonObject
}
globalsSelect: {
[slug: string]: any
}
locale: null | string
}
export type TypedCollection<T extends PayloadGeneratedTypes> = T['collections']
export type TypedGlobal<T extends PayloadGeneratedTypes> = T['globals']
export type TypedCollectionSelect<T extends PayloadGeneratedTypes> = T['collectionsSelect']
export type TypedCollectionJoins<T extends PayloadGeneratedTypes> = T['collectionsJoins']
export type TypedGlobalSelect<T extends PayloadGeneratedTypes> = T['globalsSelect']
export type TypedAuth<T extends PayloadGeneratedTypes> = T['auth']
export type CollectionSlug<T extends PayloadGeneratedTypes> = StringKeyOf<TypedCollection<T>>
export type GlobalSlug<T extends PayloadGeneratedTypes> = StringKeyOf<TypedGlobal<T>>
export type AuthCollectionSlug<T extends PayloadGeneratedTypes> = StringKeyOf<TypedAuth<T>>
export type TypedUploadCollection<T extends PayloadGeneratedTypes> = NonNever<{
[K in keyof TypedCollection<T>]:
| 'filename'
| 'filesize'
| 'mimeType'
| 'url' extends keyof TypedCollection<T>[K]
? TypedCollection<T>[K]
: never
}>
export type UploadCollectionSlug<T extends PayloadGeneratedTypes> = StringKeyOf<
TypedUploadCollection<T>
>
export type DataFromCollectionSlug<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
> = TypedCollection<T>[TSlug]
export type DataFromGlobalSlug<
T extends PayloadGeneratedTypes,
TSlug extends GlobalSlug<T>,
> = TypedGlobal<T>[TSlug]
export type SelectFromCollectionSlug<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
> = TypedCollectionSelect<T>[TSlug]
export type SelectFromGlobalSlug<
T extends PayloadGeneratedTypes,
TSlug extends GlobalSlug<T>,
> = TypedGlobalSelect<T>[TSlug]
export type TransformCollectionWithSelect<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TSelect extends SelectType,
> = TSelect extends SelectType
? TransformDataWithSelect<DataFromCollectionSlug<T, TSlug>, TSelect>
: DataFromCollectionSlug<T, TSlug>
export type TransformGlobalWithSelect<
T extends PayloadGeneratedTypes,
TSlug extends GlobalSlug<T>,
TSelect extends SelectType,
> = TSelect extends SelectType
? TransformDataWithSelect<DataFromGlobalSlug<T, TSlug>, TSelect>
: DataFromGlobalSlug<T, TSlug>
export type RequiredDataFromCollection<TData extends JsonObject> = MarkOptional<
TData,
'createdAt' | 'id' | 'sizes' | 'updatedAt'
>
export type RequiredDataFromCollectionSlug<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
> = RequiredDataFromCollection<DataFromCollectionSlug<T, TSlug>>
export type TypedLocale<T extends PayloadGeneratedTypes> = NonNullable<T['locale']>
export type JoinQuery<T extends PayloadGeneratedTypes, TSlug extends CollectionSlug<T>> =
TypedCollectionJoins<T>[TSlug] extends Record<string, string>
?
| false
| Partial<{
[K in keyof TypedCollectionJoins<T>[TSlug]]:
| {
limit?: number
sort?: string
where?: Where
}
| false
}>
: never
export type PopulateType<T extends PayloadGeneratedTypes> = Partial<TypedCollectionSelect<T>>
export type IDType<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
> = DataFromCollectionSlug<T, TSlug>['id']
export type BulkOperationResult<
T extends PayloadGeneratedTypes,
TSlug extends CollectionSlug<T>,
TSelect extends SelectType,
> = {
docs: TransformCollectionWithSelect<T, TSlug, TSelect>[]
errors: {
id: DataFromCollectionSlug<T, TSlug>['id']
message: string
}[]
}

View File

@@ -0,0 +1,72 @@
import type { SelectType, Sort, Where } from 'payload'
import { stringify } from 'qs-esm'
export type OperationArgs = {
depth?: number
draft?: boolean
fallbackLocale?: false | string
joins?: false | Record<string, unknown>
limit?: number
locale?: string
page?: number
populate?: Record<string, unknown>
select?: SelectType
sort?: Sort
where?: Where
}
export const buildSearchParams = (args: OperationArgs): string => {
const search: Record<string, unknown> = {}
if (typeof args.depth === 'number') {
search.depth = String(args.depth)
}
if (typeof args.page === 'number') {
search.page = String(args.page)
}
if (typeof args.limit === 'number') {
search.limit = String(args.limit)
}
if (typeof args.draft === 'boolean') {
search.draft = String(args.draft)
}
if (args.fallbackLocale) {
search['fallback-locale'] = String(args.fallbackLocale)
}
if (args.locale) {
search.locale = args.locale
}
if (args.sort) {
const sanitizedSort = Array.isArray(args.sort) ? args.sort.join(',') : args.sort
search.sort = sanitizedSort
}
if (args.select) {
search.select = args.select
}
if (args.where) {
search.where = args.where
}
if (args.populate) {
search.populate = args.populate
}
if (args.joins) {
search.joins = args.joins
}
if (Object.keys(search).length > 0) {
return stringify(search, { addQueryPrefix: true })
}
return ''
}

View File

@@ -0,0 +1,11 @@
export const resolveFileFromOptions = async (file: Blob | string) => {
if (typeof file === 'string') {
const response = await fetch(file)
const fileName = file.split('/').pop() ?? ''
const blob = await response.blob()
return new File([blob], fileName, { type: blob.type })
} else {
return file
}
}

View File

@@ -0,0 +1,25 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true, // Make sure typescript knows that this module depends on their references
"noEmit": false /* Do not emit outputs. */,
"emitDeclarationOnly": true,
"strict": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */
},
"exclude": [
"dist",
"build",
"tests",
"test",
"node_modules",
"eslint.config.js",
"src/**/*.spec.js",
"src/**/*.spec.jsx",
"src/**/*.spec.ts",
"src/**/*.spec.tsx"
],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
"references": [{ "path": "../payload" }]
}

19
pnpm-lock.yaml generated
View File

@@ -1372,6 +1372,22 @@ importers:
specifier: workspace:*
version: link:../payload
packages/sdk:
dependencies:
payload:
specifier: workspace:*
version: link:../payload
qs-esm:
specifier: 7.0.2
version: 7.0.2
ts-essentials:
specifier: 10.0.3
version: 10.0.3(typescript@5.7.2)
devDependencies:
'@payloadcms/eslint-config':
specifier: workspace:*
version: link:../eslint-config
packages/storage-azure:
dependencies:
'@azure/abort-controller':
@@ -1681,6 +1697,9 @@ importers:
'@payloadcms/richtext-slate':
specifier: workspace:*
version: link:../packages/richtext-slate
'@payloadcms/sdk':
specifier: workspace:*
version: link:../packages/sdk
'@payloadcms/storage-azure':
specifier: workspace:*
version: link:../packages/storage-azure

View File

@@ -28,6 +28,9 @@ export const packagePublishList = [
'email-nodemailer',
'email-resend',
// SDK,
'sdk',
// Storage
'storage-s3',
'storage-azure',

View File

@@ -0,0 +1,105 @@
'use client'
import { PayloadSDK } from '@payloadcms/sdk'
import { useAuth } from '@payloadcms/ui'
import { useEffect, useRef, useState } from 'react'
const sdk = new PayloadSDK({
baseInit: { credentials: 'include' },
baseURL: 'http://localhost:3000/api',
})
export const RealtimeUsersProvider = ({ children }) => {
const { user } = useAuth()
const [userPositions, setUserPositions] = useState<{ email: string; x: number; y: number }[]>([])
const debounceTimeout = useRef<null | number>(null)
useEffect(() => {
let unsubscribe: (() => Promise<void>) | undefined = undefined
// Emit user connection event
void sdk.realtime.emitEvent({
data: user,
event: 'userConnected',
})
const subscribe = async () => {
if (!user) {
return
}
const result = await sdk.realtime.subscribe(({ data, event }) => {
if (event === 'userMouseMove' && data.email !== user.email) {
setUserPositions((prev) => {
const updated = [...prev.filter((pos) => pos.email !== data.email), data]
return updated
})
}
})
unsubscribe = result.unsubscribe
}
void subscribe()
return () => {
if (unsubscribe) {
void unsubscribe()
}
}
}, [user])
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!user) {
return
}
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current)
}
debounceTimeout.current = window.setTimeout(() => {
sdk.realtime.emitEvent({
data: { email: user.email, x: e.clientX, y: e.clientY },
event: 'userMouseMove',
})
}, 10) // Adjust debounce delay as needed
}
window.addEventListener('mousemove', handleMouseMove)
return () => {
window.removeEventListener('mousemove', handleMouseMove)
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current)
}
}
}, [user])
return (
<>
{userPositions.map(({ email, x, y }) => (
<div
key={email}
style={{
backgroundColor: 'rgba(0, 150, 255, 0.8)',
borderRadius: '8px',
color: '#fff',
left: x,
padding: '5px 10px',
pointerEvents: 'none', // Prevent interfering with user interactions
position: 'fixed', // Changed to fixed
top: y,
transform: 'translate(-50%, -50%)',
whiteSpace: 'nowrap',
zIndex: 1000, // Ensure it's above other elements
}}
>
{email}
</div>
))}
<pre>{JSON.stringify(userPositions, null, 2)}</pre> {/* Debug rendered positions */}
{children}
</>
)
}

View File

@@ -11,36 +11,43 @@ import { MenuGlobal } from './globals/Menu/index.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfigWithDefaults({
// ...extend config here
collections: [PostsCollection, MediaCollection],
admin: {
importMap: {
baseDir: path.resolve(dirname),
export default buildConfigWithDefaults(
{
// ...extend config here
collections: [PostsCollection, MediaCollection],
admin: {
importMap: {
baseDir: path.resolve(dirname),
},
components: {
providers: [`/RealtimeUsersProvider.tsx#RealtimeUsersProvider`],
},
},
editor: lexicalEditor({}),
globals: [
// ...add more globals here
MenuGlobal,
],
onInit: async (payload) => {
await payload.create({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
})
await payload.create({
collection: postsSlug,
data: {
title: 'example post',
},
})
},
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
},
editor: lexicalEditor({}),
globals: [
// ...add more globals here
MenuGlobal,
],
onInit: async (payload) => {
await payload.create({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
})
await payload.create({
collection: postsSlug,
data: {
title: 'example post',
},
})
},
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
{ disableAutoLogin: true },
)

43
test/helpers/getSDK.ts Normal file
View File

@@ -0,0 +1,43 @@
import type { GeneratedTypes, SanitizedConfig } from 'payload'
import { REST_DELETE, REST_GET, REST_PATCH, REST_POST, REST_PUT } from '@payloadcms/next/routes'
import { PayloadSDK } from '@payloadcms/sdk'
export type TypedPayloadSDK = PayloadSDK<GeneratedTypes>
/**
* SDK with a custom fetch to run the routes directly without an HTTP server.
*/
export const getSDK = (config: SanitizedConfig) => {
const api = {
GET: REST_GET(config),
POST: REST_POST(config),
PATCH: REST_PATCH(config),
DELETE: REST_DELETE(config),
PUT: REST_PUT(config),
}
return new PayloadSDK<GeneratedTypes>({
baseURL: ``,
fetch: (path: string, init: RequestInit) => {
const [slugs, search] = path.slice(1).split('?')
const url = `${config.serverURL || 'http://localhost:3000'}${config.routes.api}/${slugs}${search ? `?${search}` : ''}`
if (init.body instanceof FormData) {
const file = init.body.get('file') as Blob
if (file && init.headers instanceof Headers) {
init.headers.set('Content-Length', file.size.toString())
}
}
const request = new Request(url, init)
const params = {
params: Promise.resolve({
slug: slugs.split('/'),
}),
}
return api[init.method.toUpperCase()](request, params)
},
})
}

View File

@@ -1,9 +1,11 @@
import type { Payload, SanitizedConfig } from 'payload'
import type { PayloadSDK } from '@payloadcms/sdk'
import type { GeneratedTypes, Payload, SanitizedConfig } from 'payload'
import path from 'path'
import { getPayload } from 'payload'
import { runInit } from '../runInit.js'
import { getSDK } from './getSDK.js'
import { NextRESTClient } from './NextRESTClient.js'
/**
@@ -13,7 +15,12 @@ export async function initPayloadInt(
dirname: string,
testSuiteNameOverride?: string,
initializePayload = true,
): Promise<{ config: SanitizedConfig; payload?: Payload; restClient?: NextRESTClient }> {
): Promise<{
config: SanitizedConfig
payload?: Payload
restClient?: NextRESTClient
sdk?: PayloadSDK<GeneratedTypes>
}> {
const testSuiteName = testSuiteNameOverride ?? path.basename(dirname)
await runInit(testSuiteName, false, true)
console.log('importing config', path.resolve(dirname, 'config.ts'))
@@ -29,5 +36,8 @@ export async function initPayloadInt(
console.log('initializing rest client')
const restClient = new NextRESTClient(payload.config)
console.log('initPayloadInt done')
return { config: payload.config, payload, restClient }
const sdk = getSDK(payload.config)
return { config: payload.config, payload, restClient, sdk }
}

View File

@@ -47,6 +47,7 @@
"@payloadcms/plugin-stripe": "workspace:*",
"@payloadcms/richtext-lexical": "workspace:*",
"@payloadcms/richtext-slate": "workspace:*",
"@payloadcms/sdk": "workspace:*",
"@payloadcms/storage-azure": "workspace:*",
"@payloadcms/storage-gcs": "workspace:*",
"@payloadcms/storage-s3": "workspace:*",

1
test/sdk/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
media

View File

@@ -0,0 +1,40 @@
import type { CollectionConfig } from 'payload'
export const postsSlug = 'posts'
export const PostsCollection: CollectionConfig = {
slug: postsSlug,
admin: {
useAsTitle: 'text',
},
access: { create: () => true, update: () => true, delete: () => true, read: () => true },
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'number',
type: 'number',
},
{
name: 'number2',
type: 'number',
},
{
name: 'group',
type: 'group',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'number',
type: 'number',
},
],
},
],
versions: true,
}

View File

@@ -0,0 +1,14 @@
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
auth: true,
admin: {
useAsTitle: 'email',
},
access: {},
fields: [
// Email added by default
// Add more fields as needed
],
}

51
test/sdk/config.ts Normal file
View File

@@ -0,0 +1,51 @@
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
import { PostsCollection } from './collections/Posts.js'
import { Users } from './collections/Users.js'
export default buildConfigWithDefaults({
admin: {
importMap: {
baseDir: path.resolve(dirname),
},
},
collections: [
Users,
PostsCollection,
{
access: { create: () => true, read: () => true, update: () => true },
slug: 'media',
upload: { staticDir: path.resolve(dirname, './media') },
fields: [],
},
],
globals: [
{
slug: 'global',
fields: [{ type: 'text', name: 'text' }],
versions: true,
},
],
localization: {
defaultLocale: 'en',
fallback: true,
locales: ['en', 'es', 'de'],
},
onInit: async (payload) => {
await payload.create({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
})
},
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})

19
test/sdk/eslint.config.js Normal file
View File

@@ -0,0 +1,19 @@
import { rootParserOptions } from '../../eslint.config.js'
import testEslintConfig from '../eslint.config.js'
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {Config[]} */
export const index = [
...testEslintConfig,
{
languageOptions: {
parserOptions: {
tsconfigRootDir: import.meta.dirname,
...rootParserOptions,
},
},
},
]
export default index

BIN
test/sdk/image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

310
test/sdk/int.spec.ts Normal file
View File

@@ -0,0 +1,310 @@
import type { Payload } from 'payload'
import { randomUUID } from 'crypto'
import { RESTClient } from 'helpers/rest.js'
import path from 'path'
import { fileURLToPath } from 'url'
import type { TypedPayloadSDK } from '../helpers/getSDK.js'
import type { Post } from './payload-types.js'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
import { createStreamableFile } from '../uploads/createStreamableFile.js'
let payload: Payload
let post: Post
let sdk: TypedPayloadSDK
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const testUserCredentials = {
email: 'test@payloadcms.com',
password: '123456',
}
describe('@payloadcms/sdk', () => {
beforeAll(async () => {
;({ payload, sdk } = await initPayloadInt(dirname))
post = await payload.create({ collection: 'posts', data: { number: 1, number2: 3 } })
await payload.create({
collection: 'users',
data: { ...testUserCredentials },
})
await payload.updateGlobal({ slug: 'global', data: { text: 'some-global' } })
})
afterAll(async () => {
if (typeof payload.db.destroy === 'function') {
await payload.db.destroy()
}
})
it('should execute find', async () => {
const result = await sdk.find({ collection: 'posts', where: { id: { equals: post.id } } })
expect(result.docs[0].id).toBe(post.id)
})
it('should execute findVersions', async () => {
const result = await sdk.findVersions({
collection: 'posts',
where: { parent: { equals: post.id } },
})
expect(result.docs[0].parent).toBe(post.id)
})
it('should execute findByID', async () => {
const result = await sdk.findByID({ collection: 'posts', id: post.id })
expect(result.id).toBe(post.id)
})
it('should execute findByID with disableErrors: true', async () => {
const result = await sdk.findByID({
disableErrors: true,
collection: 'posts',
// eslint-disable-next-line jest/no-conditional-in-test
id: typeof post.id === 'string' ? randomUUID() : 999,
})
expect(result).toBeNull()
})
it('should execute findVersionByID', async () => {
const {
docs: [version],
} = await payload.findVersions({ collection: 'posts', where: { parent: { equals: post.id } } })
const result = await sdk.findVersionByID({ collection: 'posts', id: version.id })
expect(result.id).toBe(version.id)
})
it('should execute create', async () => {
const result = await sdk.create({ collection: 'posts', data: { text: 'text' } })
expect(result.text).toBe('text')
})
it('should execute create with file', async () => {
const filePath = path.join(dirname, './image.jpg')
const { file, handle } = await createStreamableFile(filePath)
const res = await sdk.create({ collection: 'media', file, data: {} })
expect(res.id).toBeTruthy()
await handle.close()
})
it('should execute count', async () => {
const result = await sdk.count({ collection: 'posts', where: { id: { equals: post.id } } })
expect(result.totalDocs).toBe(1)
})
it('should execute update (by ID)', async () => {
const result = await sdk.update({
collection: 'posts',
id: post.id,
data: { text: 'updated-text' },
})
expect(result.text).toBe('updated-text')
})
it('should execute update (bulk)', async () => {
const result = await sdk.update({
collection: 'posts',
where: {
id: {
equals: post.id,
},
},
data: { text: 'updated-text-bulk' },
})
expect(result.docs[0].text).toBe('updated-text-bulk')
})
it('should execute delete (by ID)', async () => {
const post = await payload.create({ collection: 'posts', data: {} })
const result = await sdk.delete({ id: post.id, collection: 'posts' })
expect(result.id).toBe(post.id)
const resultLocal = await payload.findByID({
collection: 'posts',
id: post.id,
disableErrors: true,
})
expect(resultLocal).toBeNull()
})
it('should execute delete (bulk)', async () => {
const post = await payload.create({ collection: 'posts', data: {} })
const result = await sdk.delete({ where: { id: { equals: post.id } }, collection: 'posts' })
expect(result.docs[0].id).toBe(post.id)
const resultLocal = await payload.findByID({
collection: 'posts',
id: post.id,
disableErrors: true,
})
expect(resultLocal).toBeNull()
})
it('should execute restoreVersion', async () => {
const post = await payload.create({ collection: 'posts', data: { text: 'old' } })
const {
docs: [currentVersion],
} = await payload.findVersions({ collection: 'posts', where: { parent: { equals: post.id } } })
await payload.update({ collection: 'posts', id: post.id, data: { text: 'new' } })
const result = await sdk.restoreVersion({
collection: 'posts',
id: currentVersion.id,
})
expect(result.text).toBe('old')
const resultDB = await payload.findByID({ collection: 'posts', id: post.id })
expect(resultDB.text).toBe('old')
})
it('should execute findGlobal', async () => {
const result = await sdk.findGlobal({ slug: 'global' })
expect(result.text).toBe('some-global')
})
it('should execute findGlobalVersions', async () => {
const result = await sdk.findGlobalVersions({
slug: 'global',
})
expect(result.docs[0].version).toBeTruthy()
})
it('should execute findGlobalVersionByID', async () => {
const {
docs: [version],
} = await payload.findGlobalVersions({
slug: 'global',
})
const result = await sdk.findGlobalVersionByID({ id: version.id, slug: 'global' })
expect(result.id).toBe(version.id)
})
it('should execute updateGlobal', async () => {
const result = await sdk.updateGlobal({ slug: 'global', data: { text: 'some-updated-global' } })
expect(result.text).toBe('some-updated-global')
})
it('should execute restoreGlobalVersion', async () => {
await payload.updateGlobal({ slug: 'global', data: { text: 'old' } })
const {
docs: [currentVersion],
} = await payload.findGlobalVersions({
slug: 'global',
})
await payload.updateGlobal({ slug: 'global', data: { text: 'new' } })
const { version: result } = await sdk.restoreGlobalVersion({
slug: 'global',
id: currentVersion.id,
})
expect(result.text).toBe('old')
const resultDB = await payload.findGlobal({ slug: 'global' })
expect(resultDB.text).toBe('old')
})
it('should execute login', async () => {
const res = await sdk.login({
collection: 'users',
data: { email: testUserCredentials.email, password: testUserCredentials.password },
})
expect(res.user.email).toBe(testUserCredentials.email)
})
it('should execute me', async () => {
const { token } = await sdk.login({
collection: 'users',
data: { email: testUserCredentials.email, password: testUserCredentials.password },
})
const res = await sdk.me(
{ collection: 'users' },
{ headers: { Authorization: `JWT ${token}` } },
)
expect(res.user.email).toBe(testUserCredentials.email)
})
it('should execute refreshToken', async () => {
const { token } = await sdk.login({
collection: 'users',
data: { email: testUserCredentials.email, password: testUserCredentials.password },
})
const res = await sdk.refreshToken(
{ collection: 'users' },
{ headers: { Authorization: `JWT ${token}` } },
)
expect(res.user.email).toBe(testUserCredentials.email)
})
it('should execute forgotPassword and resetPassword', async () => {
const user = await payload.create({
collection: 'users',
data: { email: 'new@payloadcms.com', password: 'HOW TO rEmeMber this password' },
})
const resForgotPassword = await sdk.forgotPassword({
collection: 'users',
data: { email: user.email },
})
expect(resForgotPassword.message).toBeTruthy()
const afterForgotPassword = await payload.findByID({
showHiddenFields: true,
collection: 'users',
id: user.id,
})
expect(afterForgotPassword.resetPasswordToken).toBeTruthy()
const verifyEmailResult = await sdk.resetPassword({
collection: 'users',
data: { password: '1234567', token: afterForgotPassword.resetPasswordToken },
})
expect(verifyEmailResult.user.email).toBe(user.email)
const {
user: { email },
} = await sdk.login({
collection: 'users',
data: { email: user.email, password: '1234567' },
})
expect(email).toBe(user.email)
})
})

291
test/sdk/payload-types.ts Normal file
View File

@@ -0,0 +1,291 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
export interface Config {
auth: {
users: UserAuthOperations;
};
collections: {
users: User;
posts: Post;
media: Media;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
users: UsersSelect<false> | UsersSelect<true>;
posts: PostsSelect<false> | PostsSelect<true>;
media: MediaSelect<false> | MediaSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
};
globals: {
global: Global;
};
globalsSelect: {
global: GlobalSelect<false> | GlobalSelect<true>;
};
locale: 'en' | 'es' | 'de';
user: User & {
collection: 'users';
};
jobs: {
tasks: unknown;
workflows: unknown;
};
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: string;
updatedAt: string;
createdAt: string;
email: string;
resetPasswordToken?: string | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
password?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts".
*/
export interface Post {
id: string;
text?: string | null;
number?: number | null;
number2?: number | null;
group?: {
text?: string | null;
number?: number | null;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media".
*/
export interface Media {
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
document?:
| ({
relationTo: 'users';
value: string | User;
} | null)
| ({
relationTo: 'posts';
value: string | Post;
} | null)
| ({
relationTo: 'media';
value: string | Media;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: string | User;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
user: {
relationTo: 'users';
value: string | User;
};
key?: string | null;
value?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
loginAttempts?: T;
lockUntil?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts_select".
*/
export interface PostsSelect<T extends boolean = true> {
text?: T;
number?: T;
number2?: T;
group?:
| T
| {
text?: T;
number?: T;
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media_select".
*/
export interface MediaSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "global".
*/
export interface Global {
id: string;
text?: string | null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "global_select".
*/
export interface GlobalSelect<T extends boolean = true> {
text?: T;
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}
declare module 'payload' {
// @ts-ignore
export interface GeneratedTypes extends Config {}
}

1
test/sdk/shared.ts Normal file
View File

@@ -0,0 +1 @@
export const pagesSlug = 'pages'

View File

@@ -0,0 +1,13 @@
{
// extend your base config to share compilerOptions, etc
//"extends": "./tsconfig.json",
"compilerOptions": {
// ensure that nobody can accidentally use this config for a build
"noEmit": true
},
"include": [
// whatever paths you intend to lint
"./**/*.ts",
"./**/*.tsx"
]
}

3
test/sdk/tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "../tsconfig.json"
}

View File

@@ -31,6 +31,7 @@ export const tgzToPkgNameMap = {
'@payloadcms/plugin-stripe': 'payloadcms-plugin-stripe-*',
'@payloadcms/richtext-lexical': 'payloadcms-richtext-lexical-*',
'@payloadcms/richtext-slate': 'payloadcms-richtext-slate-*',
'@payloadcms/sdk': 'payloadcms-sdk-*',
'@payloadcms/storage-azure': 'payloadcms-storage-azure-*',
'@payloadcms/storage-gcs': 'payloadcms-storage-gcs-*',
'@payloadcms/storage-s3': 'payloadcms-storage-s3-*',

View File

@@ -37,7 +37,7 @@
],
"paths": {
"@payload-config": [
"./test/live-preview/config.ts"
"./test/_community/config.ts"
],
"@payloadcms/live-preview": [
"./packages/live-preview/src"