Compare commits

...

3 Commits

Author SHA1 Message Date
Sasha
c1338806d3 websockets 2024-12-24 04:19:26 +02:00
Sasha
958133d842 pubSub adapters, move kv-redis to redis with pubsub adapter export, remove db adapter 2024-12-24 03:45:01 +02:00
Sasha
0124afea55 feat: add KV storage adapters 2024-12-24 03:10:40 +02:00
40 changed files with 1161 additions and 15 deletions

View File

@@ -203,6 +203,13 @@ jobs:
# needed because the postgres container does not provide a healthcheck
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
redis:
image: redis:latest
ports:
- 6379:6379 # Expose Redis on port 6379
options: --health-cmd "redis-cli ping" --health-timeout 30s --health-retries 3
steps:
- uses: actions/checkout@v4
@@ -253,6 +260,10 @@ jobs:
echo "POSTGRES_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres" >> $GITHUB_ENV
if: matrix.database == 'supabase'
- name: Configure Redis
run: |
echo "REDIS_URL=redis://127.0.0.1:6379" >> $GITHUB_ENV
- name: Integration Tests
uses: nick-fields/retry@v3
env:

View File

@@ -39,6 +39,7 @@
"build:plugin-seo": "turbo build --filter \"@payloadcms/plugin-seo\"",
"build:plugin-stripe": "turbo build --filter \"@payloadcms/plugin-stripe\"",
"build:plugins": "turbo build --filter \"@payloadcms/plugin-*\"",
"build:redis": "turbo build --filter \"@payloadcms/redis\"",
"build:richtext-lexical": "turbo build --filter \"@payloadcms/richtext-lexical\"",
"build:richtext-slate": "turbo build --filter \"@payloadcms/richtext-slate\"",
"build:storage-azure": "turbo build --filter \"@payloadcms/storage-azure\"",

View File

@@ -27,6 +27,7 @@ export type ServerOnlyRootProperties = keyof Pick<
| 'graphQL'
| 'hooks'
| 'jobs'
| 'kv'
| 'logger'
| 'onInit'
| 'plugins'
@@ -67,6 +68,7 @@ export const serverOnlyConfigProperties: readonly Partial<ServerOnlyRootProperti
'graphQL',
'jobs',
'logger',
'kv',
// `admin`, `onInit`, `localization`, `collections`, and `globals` are all handled separately
]

View File

@@ -2,6 +2,8 @@ import type { JobsConfig } from '../queues/config/types/index.js'
import type { Config } from './types.js'
import defaultAccess from '../auth/defaultAccess.js'
import { inMemoryKVAdapter } from '../kv/InMemoryKVAdapter.js'
import { inMemoryPubSubAdapter } from '../pubSub/InMemoryPubSubAdapter.js'
export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
admin: {
@@ -54,8 +56,10 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
deleteJobOnComplete: true,
depth: 0,
} as JobsConfig,
kv: inMemoryKVAdapter(),
localization: false,
maxDepth: 10,
pubSub: inMemoryPubSubAdapter(),
routes: {
admin: '/admin',
api: '/api',

View File

@@ -192,6 +192,10 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
configWithDefaults.collections.push(getPreferencesCollection(config as unknown as Config))
configWithDefaults.collections.push(migrationsCollection)
if (configWithDefaults.kv.kvCollection) {
configWithDefaults.collections.push(configWithDefaults.kv.kvCollection)
}
const richTextSanitizationPromises: Array<(config: SanitizedConfig) => Promise<void>> = []
for (let i = 0; i < config.collections.length; i++) {
config.collections[i] = await sanitizeCollection(

View File

@@ -38,6 +38,8 @@ 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 { KVAdapterResult } from '../kv/index.js'
import type { PubSubAdapterResult } from '../pubSub/index.js'
import type { PayloadRequest, Where } from '../types/index.js'
import type { PayloadLogger } from '../utilities/logger.js'
@@ -974,13 +976,21 @@ export type Config = {
* @experimental There may be frequent breaking changes to this API
*/
jobs?: JobsConfig
/**
* Pass in a KV adapter for use on this project.
* @default `InMemoryDatabaseAdapter` from:
* ```ts
* import { createDatabaseKVAdapter } from 'payload'
* createDatabaseKVAdapter()
* ```
*/
kv?: KVAdapterResult
/**
* Translate your content to different languages/locales.
*
* @default false // disable localization
*/
localization?: false | LocalizationConfig
/**
* Logger options, logger options with a destination stream, or an instantiated logger instance.
*
@@ -1037,6 +1047,7 @@ export type Config = {
* @default 10
*/
maxDepth?: number
/** A function that is called immediately following startup that receives the Payload instance as its only argument. */
onInit?: (payload: Payload) => Promise<void> | void
/**
@@ -1045,6 +1056,7 @@ export type Config = {
* @see https://payloadcms.com/docs/plugins/overview
*/
plugins?: Plugin[]
pubSub?: PubSubAdapterResult
/** Control the routing structure that Payload binds itself to. */
routes?: {
/** The route for the admin panel.

View File

@@ -9,6 +9,8 @@ import { fileURLToPath } from 'node:url'
import path from 'path'
import WebSocket from 'ws'
export type { FieldState } from './admin/forms/Form.js'
import type { AuthArgs } from './auth/operations/auth.js'
import type { Result as ForgotPasswordResult } from './auth/operations/forgotPassword.js'
import type { Options as ForgotPasswordOptions } from './auth/operations/local/forgotPassword.js'
@@ -54,6 +56,8 @@ import type { Options as FindGlobalVersionByIDOptions } from './globals/operatio
import type { Options as FindGlobalVersionsOptions } from './globals/operations/local/findVersions.js'
import type { Options as RestoreGlobalVersionOptions } from './globals/operations/local/restoreVersion.js'
import type { Options as UpdateGlobalOptions } from './globals/operations/local/update.js'
import type { KVAdapter } from './kv/index.js'
import type { PubSubAdapter } from './pubSub/index.js'
import type {
ApplyDisableErrors,
JsonObject,
@@ -78,8 +82,8 @@ import { getLogger } from './utilities/logger.js'
import { serverInit as serverInitTelemetry } from './utilities/telemetry/events/serverInit.js'
import { traverseFields } from './utilities/traverseFields.js'
export type { FieldState } from './admin/forms/Form.js'
export type * from './admin/types.js'
export { default as executeAccess } from './auth/executeAccess.js'
export interface GeneratedTypes {
authUntyped: {
@@ -294,8 +298,8 @@ export class BasePayload {
}
db: DatabaseAdapter
decrypt = decrypt
decrypt = decrypt
duplicate = async <TSlug extends CollectionSlug, TSelect extends SelectFromCollectionSlug<TSlug>>(
options: DuplicateOptions<TSlug, TSelect>,
): Promise<TransformCollectionWithSelect<TSlug, TSelect>> => {
@@ -307,15 +311,15 @@ export class BasePayload {
encrypt = encrypt
// TODO: re-implement or remove?
// errorHandler: ErrorHandler
extensions: (args: {
args: OperationArgs<any>
req: graphQLRequest<unknown, unknown>
result: ExecutionResult
}) => Promise<any>
// TODO: re-implement or remove?
// errorHandler: ErrorHandler
/**
* @description Find documents with criteria
* @param options
@@ -416,6 +420,11 @@ export class BasePayload {
jobs = getJobsLocalAPI(this)
/**
* Key Value storage
*/
kv: KVAdapter
logger: Logger
login = async <TSlug extends CollectionSlug>(
@@ -425,6 +434,8 @@ export class BasePayload {
return login<TSlug>(this, options)
}
pubSub: PubSubAdapter
resetPassword = async <TSlug extends CollectionSlug>(
options: ResetPasswordOptions<TSlug>,
): Promise<ResetPasswordResult> => {
@@ -618,6 +629,10 @@ export class BasePayload {
this.db = this.config.db.init({ payload: this })
this.db.payload = this
this.kv = this.config.kv.init({ payload: this })
this.pubSub = this.config.pubSub.init({ payload: this })
if (this.db?.init) {
await this.db.init()
}
@@ -881,7 +896,6 @@ interface RequestContext {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface DatabaseAdapter extends BaseDatabaseAdapter {}
export type { Payload, RequestContext }
export { default as executeAccess } from './auth/executeAccess.js'
export { executeAuthStrategies } from './auth/executeAuthStrategies.js'
export { getAccessResults } from './auth/getAccessResults.js'
export { getFieldsToSign } from './auth/getFieldsToSign.js'
@@ -898,7 +912,6 @@ export { registerFirstUserOperation } from './auth/operations/registerFirstUser.
export { resetPasswordOperation } from './auth/operations/resetPassword.js'
export { unlockOperation } from './auth/operations/unlock.js'
export { verifyEmailOperation } from './auth/operations/verifyEmail.js'
export type {
AuthStrategyFunction,
AuthStrategyFunctionArgs,
@@ -919,8 +932,8 @@ export type {
} from './auth/types.js'
export { generateImportMap } from './bin/generateImportMap/index.js'
export type { ImportMap } from './bin/generateImportMap/index.js'
export type { ImportMap } from './bin/generateImportMap/index.js'
export { genImportMapIterateFields } from './bin/generateImportMap/iterateFields.js'
export {
@@ -967,6 +980,7 @@ export type {
TypeWithID,
TypeWithTimestamps,
} from './collections/config/types.js'
export { createDataloaderCacheKey, getDataLoader } from './collections/dataloader.js'
export { countOperation } from './collections/operations/count.js'
export { createOperation } from './collections/operations/create.js'
@@ -988,8 +1002,8 @@ export {
serverOnlyAdminConfigProperties,
serverOnlyConfigProperties,
} from './config/client.js'
export { defaults } from './config/defaults.js'
export { sanitizeConfig } from './config/sanitize.js'
export type * from './config/types.js'
export { combineQueries } from './database/combineQueries.js'
@@ -1098,8 +1112,8 @@ export {
ValidationErrorName,
} from './errors/index.js'
export type { ValidationFieldError } from './errors/index.js'
export { baseBlockFields } from './fields/baseFields/baseBlockFields.js'
export { baseIDField } from './fields/baseFields/baseIDField.js'
export {
createClientField,
@@ -1209,16 +1223,16 @@ export type {
ValidateOptions,
ValueWithRelation,
} from './fields/config/types.js'
export { getDefaultValue } from './fields/getDefaultValue.js'
export { traverseFields as afterChangeTraverseFields } from './fields/hooks/afterChange/traverseFields.js'
export { promise as afterReadPromise } from './fields/hooks/afterRead/promise.js'
export { traverseFields as afterReadTraverseFields } from './fields/hooks/afterRead/traverseFields.js'
export { traverseFields as beforeChangeTraverseFields } from './fields/hooks/beforeChange/traverseFields.js'
export { traverseFields as beforeValidateTraverseFields } from './fields/hooks/beforeValidate/traverseFields.js'
export { default as sortableFieldTypes } from './fields/sortableFieldTypes.js'
export { validations } from './fields/validations.js'
export type {
ArrayFieldValidation,
BlocksFieldValidation,
@@ -1250,7 +1264,6 @@ export type {
UploadFieldValidation,
UsernameFieldValidation,
} from './fields/validations.js'
export {
type ClientGlobalConfig,
createClientGlobalConfig,
@@ -1274,9 +1287,13 @@ export type {
export { docAccessOperation as docAccessOperationGlobal } from './globals/operations/docAccess.js'
export { findOneOperation } from './globals/operations/findOne.js'
export { findVersionByIDOperation as findVersionByIDOperationGlobal } from './globals/operations/findVersionByID.js'
export { findVersionsOperation as findVersionsOperationGlobal } from './globals/operations/findVersions.js'
export { restoreVersionOperation as restoreVersionOperationGlobal } from './globals/operations/restoreVersion.js'
export { updateOperation as updateOperationGlobal } from './globals/operations/update.js'
export * from './kv/index.js'
export * from './kv/InMemoryKVAdapter.js'
export type {
CollapsedPreferences,
DocumentPreferences,
@@ -1286,6 +1303,8 @@ export type {
PreferenceUpdateRequest,
TabsPreferences,
} from './preferences/types.js'
export type { PubSubAdapter, PubSubAdapterResult } from './pubSub/index.js'
export { InMemoryPubSubAdapter, inMemoryPubSubAdapter } from './pubSub/InMemoryPubSubAdapter.js'
export type { JobsConfig, RunJobAccess, RunJobAccessArgs } from './queues/config/types/index.js'
export type {
RunTaskFunction,

View File

@@ -0,0 +1,42 @@
/* eslint-disable @typescript-eslint/require-await */
import type { KVAdapter, KVAdapterResult, KVStoreValue } from '../index.js'
export class InMemoryKVAdapter implements KVAdapter {
store = new Map<string, KVStoreValue>()
async clear(): Promise<void> {
this.store.clear()
}
async delete(key: string): Promise<void> {
this.store.delete(key)
}
async get(key: string): Promise<KVStoreValue | null> {
const value = this.store.get(key)
if (typeof value === 'undefined') {
return null
}
return value
}
async has(key: string): Promise<boolean> {
return this.store.has(key)
}
async keys(): Promise<string[]> {
return Array.from(this.store.keys())
}
async set(key: string, value: KVStoreValue): Promise<void> {
this.store.set(key, value)
}
}
export const inMemoryKVAdapter = (): KVAdapterResult => {
return {
init: () => new InMemoryKVAdapter(),
}
}

View File

@@ -0,0 +1,54 @@
import type { CollectionConfig } from '../collections/config/types.js'
import type { Payload } from '../types/index.js'
export type KVStoreValue = NonNullable<unknown>
export interface KVAdapter {
/**
* Clears all entries in the store.
* @returns A promise that resolves once the store is cleared.
*/
clear(): Promise<void>
/**
* Deletes a value from the store by its key.
* @param key - The key to delete.
* @returns A promise that resolves once the key is deleted.
*/
delete(key: string): Promise<void>
/**
* Retrieves a value from the store by its key.
* @param key - The key to look up.
* @returns A promise that resolves to the value, or `null` if not found.
*/
get(key: string): Promise<KVStoreValue | null>
/**
* Checks if a key exists in the store.
* @param key - The key to check.
* @returns A promise that resolves to `true` if the key exists, otherwise `false`.
*/
has(key: string): Promise<boolean>
/**
* Retrieves all the keys in the store.
* @returns A promise that resolves to an array of keys.
*/
keys(): Promise<string[]>
/**
* Sets a value in the store with the given key.
* @param key - The key to associate with the value.
* @param value - The value to store.
* @returns A promise that resolves once the value is stored.
*/
set(key: string, value: KVStoreValue): Promise<void>
}
export interface KVAdapterResult {
init(args: { payload: Payload }): KVAdapter
/** Adapter can create additional collection if needed */
kvCollection?: CollectionConfig
}

View File

@@ -0,0 +1,7 @@
// Use strict: true for new features. This overrides options only for this directory.
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"strict": true
}
}

View File

@@ -0,0 +1,32 @@
/* eslint-disable @typescript-eslint/require-await */
import type { PubSubAdapter, PubSubAdapterResult } from './index.js'
export class InMemoryPubSubAdapter implements PubSubAdapter {
private subscriptions: Record<string, ((message: string) => void)[]> = {}
async publish(channel: string, message: string): Promise<void> {
const subscribers = this.subscriptions[channel] || []
for (const subscriber of subscribers) {
subscriber(message)
}
}
async subscribe(channel: string, callback: (message: string) => void): Promise<void> {
if (!this.subscriptions[channel]) {
this.subscriptions[channel] = []
}
this.subscriptions[channel].push(callback)
}
async unsubscribe(channel: string): Promise<void> {
delete this.subscriptions[channel]
}
}
export const inMemoryPubSubAdapter = (): PubSubAdapterResult => {
return {
init: () => new InMemoryPubSubAdapter(),
}
}

View File

@@ -0,0 +1,11 @@
import type { Payload } from '../types/index.js'
export type PubSubAdapter = {
publish: (channel: string, message: string) => Promise<void>
subscribe: (channel: string, callback: (message: string) => void) => Promise<void>
unsubscribe: (channel: string) => Promise<void>
}
export type PubSubAdapterResult = {
init: (args: { payload: Payload }) => PubSubAdapter
}

View File

@@ -0,0 +1,7 @@
// Use strict: true for new features. This overrides options only for this directory.
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"strict": true
}
}

View File

View File

@@ -0,0 +1,7 @@
// Use strict: true for new features. This overrides options only for this directory.
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"strict": true
}
}

View File

@@ -0,0 +1,139 @@
import type { IncomingMessage, Server } from 'http'
import { default as WebSocket } from 'ws'
import type { SanitizedConfig } from '../config/types.js'
import type { PayloadRequest } from '../index.js'
import { createLocalReq, getPayload } from '../index.js'
const createLocalReqFromNode = async (
request: IncomingMessage,
config: SanitizedConfig,
): Promise<PayloadRequest> => {
const headers = new Headers()
for (const [key, value] of Object.entries(request.headers)) {
if (Array.isArray(value)) {
for (const v of value) {
headers.append(key, v)
}
} else {
headers.set(key, value)
}
}
const payload = await getPayload({ config })
const authResult = await payload.auth({ headers })
return createLocalReq(
{
req: {
headers,
url: request.url,
},
user: authResult.user,
},
payload,
)
}
const availableChannels = ['payload-data']
const setPayloadRequest = (ws: WebSocket, req: PayloadRequest) => {
ws['payloadReq'] = req
}
const getPayloadRequest = (ws: WebSocket): PayloadRequest => {
return ws['payloadReq']
}
type SubscribeEvent = {
data: {
channel: string
opts?: Record<string, unknown>
}
type: 'subscribe'
}
type UnsubscribeEvent = {
data: {
channel: string
}
type: 'unsubscribe'
}
type Event = SubscribeEvent | UnsubscribeEvent
const parseEvent = (data: WebSocket.RawData): Event => {
if (typeof data !== 'string') {
return null
}
let parsedJSON: Event | null = null
try {
parsedJSON = JSON.parse(data)
} catch {
return null
}
if (typeof parsedJSON !== 'object' || parsedJSON === null) {
return null
}
return parsedJSON
}
export const attachWebsocket = async ({
config,
debug,
server,
}: {
config: SanitizedConfig
debug?: boolean
server: Server
}) => {
const payload = await getPayload({ config })
const wss = new WebSocket.Server({ noServer: true })
// await payload.pubSub.subscribe('payload', (message) => {})
wss.on('connection', (ws) => {
const { payload } = getPayloadRequest(ws)
let channels: string[] = []
ws.on('message', (message) => {
const event = parseEvent(message)
if (!event) {
return
}
if (debug) {
payload.logger.debug('Received event', event)
}
if (event.type == 'subscribe') {
if (availableChannels.includes(event.data.channel)) {
channels.push(event.data.channel)
}
} else if (event.type === 'unsubscribe') {
channels = channels.filter((channel) => channel !== event.data.channel)
}
})
})
server.on('upgrade', async (request, socket, head) => {
const req = await createLocalReqFromNode(request, config)
if (req.pathname.startsWith('/api/realtime')) {
wss.handleUpgrade(request, socket, head, (ws) => {
setPayloadRequest(ws, req)
wss.emit('connection', ws, request)
})
}
})
}

View File

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

15
packages/redis/.swcrc Normal file
View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": true,
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": true,
"dts": true
}
},
"module": {
"type": "es6"
}
}

22
packages/redis/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.

33
packages/redis/README.md Normal file
View File

@@ -0,0 +1,33 @@
# Redis KV Adapter for Payload (beta)
This package provides a way to use [Redis](https://redis.io) as a KV adapter with Payload.
## Installation
```sh
pnpm add @payloadcms/kv-redis
```
## Usage
```ts
import { redisKVAdapter } from '@payloadcms/kv-redis'
export default buildConfig({
collections: [Media],
kv: redisKVAdapter({
// Redis connection URL. Defaults to process.env.REDIS_URL
redisURL: 'redis://localhost:6379',
// Optional prefix for Redis keys to isolate the store. Defaults to 'payload-kv'
prefix: 'kv-storage',
}),
})
```
Then you can access the KV storage using `payload.kv`:
```ts
await payload.kv.set('key', { value: 1 })
const data = await payload.kv.get('key')
payload.loger.info(data)
```

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

View File

@@ -0,0 +1,66 @@
{
"name": "@payloadcms/redis",
"version": "3.6.0",
"description": "Payload storage adapter for uploadthing",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/storage-uploadthing"
},
"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 build:types && pnpm build:swc",
"build:clean": "find . \\( -type d \\( -name build -o -name dist -o -name .cache \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} + && pnpm build",
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf {dist,*.tsbuildinfo}",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"dependencies": {
"ioredis": "^5.4.1"
},
"devDependencies": {
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "workspace:*"
},
"engines": {
"node": "^18.20.2 || >=20.9.0"
},
"publishConfig": {
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}

View File

@@ -0,0 +1,79 @@
import type { KVAdapter, KVAdapterResult, KVStoreValue } from 'payload'
import { Redis } from 'ioredis'
export class RedisKVAdapter implements KVAdapter {
redisClient: Redis
constructor(
readonly keyPrefix: string,
redisURL: string,
) {
this.redisClient = new Redis(redisURL)
}
async clear(): Promise<void> {
const keys = await this.redisClient.keys(`${this.keyPrefix}*`)
if (keys.length > 0) {
await this.redisClient.del(keys)
}
}
async delete(key: string): Promise<void> {
await this.redisClient.del(`${this.keyPrefix}${key}`)
}
async get(key: string): Promise<KVStoreValue | null> {
const data = await this.redisClient.get(`${this.keyPrefix}${key}`)
if (data === null) {
return data
}
return JSON.parse(data)
}
async has(key: string): Promise<boolean> {
const exists = await this.redisClient.exists(`${this.keyPrefix}${key}`)
return exists === 1
}
async keys(): Promise<string[]> {
const prefixedKeys = await this.redisClient.keys(`${this.keyPrefix}*`)
if (this.keyPrefix) {
return prefixedKeys.map((key) => key.replace(this.keyPrefix, ''))
}
return prefixedKeys
}
async set(key: string, data: KVStoreValue): Promise<void> {
await this.redisClient.set(`${this.keyPrefix}${key}`, JSON.stringify(data))
}
}
export type RedisKVAdapterOptions = {
/**
* Optional prefix for Redis keys to isolate the store
*
* @default 'payload-kv:'
*/
keyPrefix?: string
/** Redis connection URL (e.g., 'redis://localhost:6379'). Defaults to process.env.REDIS_URL */
redisURL?: string
}
export const redisKVAdapter = (options: RedisKVAdapterOptions = {}): KVAdapterResult => {
const keyPrefix = options.keyPrefix ?? 'payload-kv:'
const redisURL = options.redisURL ?? process.env.REDIS_URL
if (!redisURL) {
throw new Error('redisURL or REDIS_URL env variable is required')
}
return {
init: () => new RedisKVAdapter(keyPrefix, redisURL),
}
}

View File

@@ -0,0 +1,49 @@
import type { PubSubAdapter, PubSubAdapterResult } from 'payload'
import { Redis } from 'ioredis'
export class RedisPubSubAdapter implements PubSubAdapter {
listenerClient: Redis
publisherClient: Redis
constructor(redisURL: string) {
this.listenerClient = new Redis(redisURL)
this.publisherClient = new Redis(redisURL)
}
async publish(channel: string, message: string): Promise<void> {
await this.publisherClient.publish(channel, message)
}
async subscribe(channel: string, callback: (message: string) => void): Promise<void> {
await this.listenerClient.subscribe(channel)
this.listenerClient.on('message', (subChannel, message) => {
if (subChannel === channel) {
callback(message)
}
})
}
async unsubscribe(channel: string): Promise<void> {
await this.listenerClient.unsubscribe(channel)
}
}
export type RedisPubSubAdapterOptions = {
/** Redis connection URL (e.g., 'redis://localhost:6379'). Defaults to process.env.REDIS_URL */
redisURL?: string
}
export const redisPubSubAdapter = (
options: RedisPubSubAdapterOptions = {},
): PubSubAdapterResult => {
const redisURL = options.redisURL ?? process.env.REDIS_URL
if (!redisURL) {
throw new Error('redisURL or REDIS_URL env variable is required')
}
return {
init: () => new RedisPubSubAdapter(redisURL),
}
}

View File

@@ -0,0 +1,6 @@
export { redisKVAdapter, RedisKVAdapter, type RedisKVAdapterOptions } from './RedisKVAdapter.js'
export {
RedisPubSubAdapter,
redisPubSubAdapter,
type RedisPubSubAdapterOptions,
} from './RedisPubSubAdapter.js'

View File

@@ -0,0 +1,14 @@
{
"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,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
"strict": true
},
"exclude": ["dist", "node_modules"],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
"references": [{ "path": "../payload" }]
}

77
pnpm-lock.yaml generated
View File

@@ -1182,6 +1182,16 @@ importers:
specifier: workspace:*
version: link:../payload
packages/redis:
dependencies:
ioredis:
specifier: ^5.4.1
version: 5.4.1
devDependencies:
payload:
specifier: workspace:*
version: link:../payload
packages/richtext-lexical:
dependencies:
'@faceless-ui/modal':
@@ -1679,6 +1689,9 @@ importers:
'@payloadcms/plugin-stripe':
specifier: workspace:*
version: link:../packages/plugin-stripe
'@payloadcms/redis':
specifier: workspace:*
version: link:../packages/redis
'@payloadcms/richtext-lexical':
specifier: workspace:*
version: link:../packages/richtext-lexical
@@ -3593,6 +3606,9 @@ packages:
cpu: [x64]
os: [win32]
'@ioredis/commands@1.2.0':
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@@ -5907,6 +5923,10 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
cluster-key-slot@1.1.2:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
co@4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@@ -6186,6 +6206,10 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
@@ -7305,6 +7329,10 @@ packages:
resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
engines: {node: '>= 0.10'}
ioredis@5.4.1:
resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==}
engines: {node: '>=12.22.0'}
ip-address@9.0.5:
resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==}
engines: {node: '>= 12'}
@@ -7887,9 +7915,15 @@ packages:
lodash.deburr@4.1.0:
resolution: {integrity: sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==}
lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
lodash.get@4.4.2:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
lodash.memoize@4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
@@ -8906,6 +8940,14 @@ packages:
resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==}
engines: {node: '>= 0.10'}
redis-errors@1.2.0:
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
engines: {node: '>=4'}
redis-parser@3.0.0:
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
engines: {node: '>=4'}
refa@0.12.1:
resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
@@ -9453,6 +9495,9 @@ packages:
resolution: {integrity: sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==}
engines: {node: '>=6'}
standard-as-callback@2.1.0:
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
state-local@1.0.7:
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
@@ -12562,6 +12607,8 @@ snapshots:
'@img/sharp-win32-x64@0.33.5':
optional: true
'@ioredis/commands@1.2.0': {}
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@@ -15521,6 +15568,8 @@ snapshots:
clsx@2.1.1: {}
cluster-key-slot@1.1.2: {}
co@4.6.0: {}
collect-v8-coverage@1.0.2: {}
@@ -15779,6 +15828,8 @@ snapshots:
delayed-stream@1.0.0: {}
denque@2.1.0: {}
dequal@2.0.3: {}
destr@2.0.3: {}
@@ -17110,6 +17161,20 @@ snapshots:
interpret@1.4.0: {}
ioredis@5.4.1:
dependencies:
'@ioredis/commands': 1.2.0
cluster-key-slot: 1.1.2
debug: 4.3.7
denque: 2.1.0
lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0
redis-errors: 1.2.0
redis-parser: 3.0.0
standard-as-callback: 2.1.0
transitivePeerDependencies:
- supports-color
ip-address@9.0.5:
dependencies:
jsbn: 1.1.0
@@ -17891,8 +17956,12 @@ snapshots:
lodash.deburr@4.1.0: {}
lodash.defaults@4.2.0: {}
lodash.get@4.4.2: {}
lodash.isarguments@3.1.0: {}
lodash.memoize@4.1.2: {}
lodash.merge@4.6.2: {}
@@ -19091,6 +19160,12 @@ snapshots:
dependencies:
resolve: 1.22.8
redis-errors@1.2.0: {}
redis-parser@3.0.0:
dependencies:
redis-errors: 1.2.0
refa@0.12.1:
dependencies:
'@eslint-community/regexpp': 4.12.1
@@ -19638,6 +19713,8 @@ snapshots:
dependencies:
type-fest: 0.7.1
standard-as-callback@2.1.0: {}
state-local@1.0.7: {}
std-env@3.7.0: {}

View File

@@ -29,5 +29,6 @@ if (!process.env.PAYLOAD_DATABASE) {
// Mutate env so we can use conditions by DB adapter in tests properly without ignoring // eslint no-jest-conditions.
process.env.PAYLOAD_DATABASE = 'mongodb'
}
process.env.REDIS_URL = process.env.REDIS_URL ?? 'redis://127.0.0.1:6379'
generateDatabaseAdapter(process.env.PAYLOAD_DATABASE)

2
test/kv/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/media
/media-gif

21
test/kv/config.ts Normal file
View File

@@ -0,0 +1,21 @@
import { fileURLToPath } from 'node:url'
import path from 'path'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfigWithDefaults({
// ...extend config here
collections: [],
admin: {
importMap: {
baseDir: path.resolve(dirname),
},
},
globals: [],
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})

19
test/kv/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: {
...rootParserOptions,
tsconfigRootDir: import.meta.dirname,
},
},
},
]
export default index

119
test/kv/int.spec.ts Normal file
View File

@@ -0,0 +1,119 @@
import type { KVAdapterResult, Payload, PubSubAdapterResult } from 'payload'
import { RedisKVAdapter, redisKVAdapter, redisPubSubAdapter } from '@payloadcms/redis'
import path from 'path'
import { inMemoryKVAdapter, inMemoryPubSubAdapter } from 'payload'
import { fileURLToPath } from 'url'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
let payload: Payload
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
describe('KV / PubSub Adapters', () => {
// --__--__--__--__--__--__--__--__--__
// Boilerplate test setup/teardown
// --__--__--__--__--__--__--__--__--__
beforeAll(async () => {
const initialized = await initPayloadInt(dirname)
;({ payload } = initialized)
})
afterAll(async () => {
if (typeof payload.db.destroy === 'function') {
await payload.db.destroy()
}
})
const testKVAdapter = async (adapter: KVAdapterResult) => {
if (adapter) {
payload.kv = adapter.init({ payload })
}
await payload.kv.set('my-key-1', { userID: 1 })
await payload.kv.set('my-key-2', { userID: 2 })
expect(await payload.kv.get('my-key-1')).toStrictEqual({ userID: 1 })
expect(await payload.kv.get('my-key-2')).toStrictEqual({ userID: 2 })
expect(await payload.kv.get('my-key-3')).toBeNull()
expect(await payload.kv.has('my-key-1')).toBeTruthy()
expect(await payload.kv.has('my-key-2')).toBeTruthy()
expect(await payload.kv.has('my-key-3')).toBeFalsy()
let keys = await payload.kv.keys()
expect(keys).toHaveLength(2)
expect(keys).toContain('my-key-1')
expect(keys).toContain('my-key-2')
await payload.kv.set('my-key-1', { userID: 10 })
expect(await payload.kv.get('my-key-1')).toStrictEqual({ userID: 10 })
await payload.kv.delete('my-key-1')
expect(await payload.kv.get('my-key-1')).toBeNull()
expect(await payload.kv.has('my-key-1')).toBeFalsy()
keys = await payload.kv.keys()
expect(keys).toHaveLength(1)
expect(keys).toContain('my-key-2')
await payload.kv.clear()
expect(await payload.kv.get('my-key-2')).toBeNull()
expect(await payload.kv.has('my-key-2')).toBeFalsy()
keys = await payload.kv.keys()
expect(keys).toHaveLength(0)
if (payload.kv instanceof RedisKVAdapter) {
await payload.kv.redisClient.quit()
}
return true
}
const testPubSubAdapter = async (adapter?: PubSubAdapterResult) => {
payload.pubSub = adapter.init({ payload })
const channel = 'my-channel'
let receivedMessage: null | string = null
// Subscribe to the channel
await payload.pubSub.subscribe(channel, (message) => {
receivedMessage = message
})
// Publish a message to the channel
await payload.pubSub.publish(channel, 'Hello, world!')
let retries = 0
while (!receivedMessage) {
if (retries++ > 10) {
break
}
await new Promise((resolve) => setTimeout(resolve, 30))
}
expect(receivedMessage).toBe('Hello, world!')
await payload.pubSub.unsubscribe(channel)
return true
}
it('inMemoryKVAdapter', async () => {
expect(await testKVAdapter(inMemoryKVAdapter())).toBeTruthy()
})
it('redisKVAdapter', async () => {
expect(await testKVAdapter(redisKVAdapter())).toBeTruthy()
})
it('inMemoryPubSubAdapter', async () => {
expect(await testPubSubAdapter(inMemoryPubSubAdapter())).toBeTruthy()
})
it('redisPubSubAdapter', async () => {
expect(await testPubSubAdapter(redisPubSubAdapter())).toBeTruthy()
})
})

213
test/kv/payload-types.ts Normal file
View File

@@ -0,0 +1,213 @@
/* 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;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
'payload-kv': PayloadKv;
};
collectionsJoins: {};
collectionsSelect: {
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
};
db: {
defaultIDType: string;
};
globals: {};
globalsSelect: {};
locale: null;
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` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
document?: {
relationTo: 'users';
value: string | User;
} | 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` "payload-kv".
*/
export interface PayloadKv {
id: string;
key: string;
data:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
}
/**
* 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` "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` "payload-kv_select".
*/
export interface PayloadKvSelect<T extends boolean = true> {
key?: T;
data?: 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 {}
}

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/kv/tsconfig.json Normal file
View File

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

9
test/kv/types.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
import type { RequestContext as OriginalRequestContext } from 'payload'
declare module 'payload' {
// Create a new interface that merges your additional fields with the original one
export interface RequestContext extends OriginalRequestContext {
myObject?: string
// ...
}
}

View File

@@ -45,6 +45,7 @@
"@payloadcms/plugin-sentry": "workspace:*",
"@payloadcms/plugin-seo": "workspace:*",
"@payloadcms/plugin-stripe": "workspace:*",
"@payloadcms/redis": "workspace:*",
"@payloadcms/richtext-lexical": "workspace:*",
"@payloadcms/richtext-slate": "workspace:*",
"@payloadcms/storage-azure": "workspace:*",

View File

@@ -19,6 +19,7 @@ export const tgzToPkgNameMap = {
'@payloadcms/graphql': 'payloadcms-graphql-*',
'@payloadcms/live-preview': 'payloadcms-live-preview-*',
'@payloadcms/live-preview-react': 'payloadcms-live-preview-react-*',
'@payloadcms/redis': 'payloadcms-redis-*',
'@payloadcms/next': 'payloadcms-next-*',
'@payloadcms/payload-cloud': 'payloadcms-payload-cloud-*',
'@payloadcms/plugin-cloud-storage': 'payloadcms-plugin-cloud-storage-*',

View File

@@ -28,7 +28,7 @@
}
],
"paths": {
"@payload-config": ["./test/live-preview/config.ts"],
"@payload-config": ["./test/_community/config.ts"],
"@payloadcms/live-preview": ["./packages/live-preview/src"],
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],

View File

@@ -31,6 +31,9 @@
{
"path": "./packages/live-preview-vue"
},
{
"path": "./packages/redis"
},
{
"path": "./packages/next"
},