chore: move to eslint v9 (#7041)

- Upgrades eslint from v8 to v9
- Upgrades all other eslint packages. We will have to do a new
full-project lint, as new rules have been added
- Upgrades husky from v8 to v9
- Upgrades lint-staged from v14 to v15
- Moves the old .eslintrc.cjs file format to the new eslint.config.js
flat file format.

Previously, we were very specific regarding which rules are applied to
which files. Now that `extends` is no longer a thing, I have to use
deepMerge & imports instead.

This is rather uncommon and is not a documented pattern - e.g.
typescript-eslint docs want us to add the default typescript-eslint
rules to the top-level & then disable it in files using the
disable-typechecked config.

However, I hate this opt-out approach. The way I did it here adds a lot
of clarity as to which rules are applied to which files, and is pretty
easy to read. Much less black magic

## .eslintignore

These files are no longer supported (see
https://eslint.org/docs/latest/use/configure/migration-guide#ignoring-files).
I moved the entries to the ignores property in the eslint config. => one
less file in each package folder!
This commit is contained in:
Alessio Gravili
2024-07-09 09:50:37 -04:00
committed by GitHub
parent bd5f5a2d4b
commit 1038e1c228
238 changed files with 2915 additions and 1978 deletions

View File

@@ -1,8 +0,0 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
ignorePatterns: ['payload-types.ts'],
parserOptions: {
project: ['./tsconfig.eslint.json'],
tsconfigRootDir: __dirname,
},
}

View File

@@ -4,6 +4,8 @@ const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
import type { FieldAccess } from 'payload'
import type { Config, User } from './payload-types.js'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
import { TestButton } from './TestButton.js'
@@ -34,9 +36,9 @@ import {
const openAccess = {
create: () => true,
delete: () => true,
read: () => true,
update: () => true,
delete: () => true,
}
const PublicReadabilityAccess: FieldAccess = ({ req: { user }, siblingData }) => {
@@ -51,83 +53,20 @@ const UseRequestHeadersAccess: FieldAccess = ({ req: { headers } }) => {
return !!headers && headers.get('authorization') === requestHeaders.get('authorization')
}
function isUser(user: Config['user']): user is {
collection: 'users'
} & User {
return user?.collection === 'users'
}
export default buildConfigWithDefaults({
admin: {
user: 'users',
autoLogin: false,
user: 'users',
},
globals: [
{
slug: 'settings',
fields: [
{
type: 'checkbox',
name: 'test',
label: 'Allow access to test global',
},
],
admin: {
components: {
elements: {
SaveButton: TestButton,
},
},
},
},
{
slug: 'test',
fields: [],
access: {
read: async ({ req: { payload } }) => {
const access = await payload.findGlobal({ slug: 'settings' })
return Boolean(access.test)
},
},
},
{
slug: readOnlyGlobalSlug,
fields: [
{
name: 'name',
type: 'text',
},
],
access: {
read: () => true,
update: () => false,
},
},
{
slug: userRestrictedGlobalSlug,
fields: [
{
name: 'name',
type: 'text',
},
],
access: {
read: () => true,
update: ({ req, data }) => data?.name === req.user?.email,
},
},
{
slug: readNotUpdateGlobalSlug,
fields: [
{
name: 'name',
type: 'text',
},
],
access: {
read: () => true,
update: () => false,
},
},
],
collections: [
{
slug: 'users',
auth: true,
access: {
// admin: () => true,
admin: async ({ req }) => {
@@ -141,20 +80,21 @@ export default buildConfigWithDefaults({
})
},
},
auth: true,
fields: [
{
name: 'roles',
type: 'select',
hasMany: true,
options: ['admin', 'user'],
defaultValue: ['user'],
access: {
create: ({ req }) => req.user?.roles?.includes('admin'),
create: ({ req }) => isUser(req.user) && req.user?.roles?.includes('admin'),
read: () => false,
update: ({ req }) => {
return req.user?.roles?.includes('admin')
return isUser(req.user) && req.user?.roles?.includes('admin')
},
},
defaultValue: ['user'],
hasMany: true,
options: ['admin', 'user'],
},
],
},
@@ -179,16 +119,16 @@ export default buildConfigWithDefaults({
},
},
{
type: 'group',
name: 'group',
type: 'group',
fields: [
{
name: 'restrictedGroupText',
type: 'text',
access: {
create: () => false,
read: () => false,
update: () => false,
create: () => false,
},
},
],
@@ -200,27 +140,27 @@ export default buildConfigWithDefaults({
name: 'restrictedRowText',
type: 'text',
access: {
create: () => false,
read: () => false,
update: () => false,
create: () => false,
},
},
],
},
{
type: 'collapsible',
label: 'Access',
fields: [
{
name: 'restrictedCollapsibleText',
type: 'text',
access: {
create: () => false,
read: () => false,
update: () => false,
create: () => false,
},
},
],
label: 'Access',
},
],
},
@@ -234,71 +174,59 @@ export default buildConfigWithDefaults({
{
name: 'userRestrictedDocs',
type: 'relationship',
relationTo: userRestrictedCollectionSlug,
hasMany: true,
relationTo: userRestrictedCollectionSlug,
},
{
name: 'createNotUpdateDocs',
type: 'relationship',
relationTo: createNotUpdateCollectionSlug,
hasMany: true,
relationTo: createNotUpdateCollectionSlug,
},
],
},
{
slug: fullyRestrictedSlug,
access: {
create: () => false,
delete: () => false,
read: () => false,
update: () => false,
},
fields: [
{
name: 'name',
type: 'text',
},
],
access: {
create: () => false,
read: () => false,
update: () => false,
delete: () => false,
},
},
{
slug: readOnlySlug,
access: {
create: () => false,
delete: () => false,
read: () => true,
update: () => false,
},
fields: [
{
name: 'name',
type: 'text',
},
],
access: {
create: () => false,
read: () => true,
update: () => false,
delete: () => false,
},
},
{
slug: userRestrictedCollectionSlug,
admin: {
useAsTitle: 'name',
},
fields: [
{
name: 'name',
type: 'text',
},
],
access: {
create: () => true,
delete: () => false,
read: () => true,
update: ({ req }) => ({
name: {
equals: req.user?.email,
},
}),
delete: () => false,
},
},
{
slug: createNotUpdateCollectionSlug,
admin: {
useAsTitle: 'name',
},
@@ -308,27 +236,27 @@ export default buildConfigWithDefaults({
type: 'text',
},
],
access: {
create: () => true,
read: () => true,
update: () => false,
delete: () => false,
},
},
{
slug: restrictedVersionsSlug,
versions: true,
slug: createNotUpdateCollectionSlug,
access: {
create: () => true,
delete: () => false,
read: () => true,
update: () => false,
},
admin: {
useAsTitle: 'name',
},
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'hidden',
type: 'checkbox',
hidden: true,
},
],
},
{
slug: restrictedVersionsSlug,
access: {
read: ({ req: { user } }) => {
if (user) return true
@@ -349,6 +277,18 @@ export default buildConfigWithDefaults({
}
},
},
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'hidden',
type: 'checkbox',
hidden: true,
},
],
versions: true,
},
{
slug: siblingDataSlug,
@@ -363,8 +303,8 @@ export default buildConfigWithDefaults({
fields: [
{
name: 'allowPublicReadability',
label: 'Allow Public Readability',
type: 'checkbox',
label: 'Allow Public Readability',
},
{
name: 'text',
@@ -383,9 +323,9 @@ export default buildConfigWithDefaults({
slug: relyOnRequestHeadersSlug,
access: {
create: UseRequestHeadersAccess,
delete: UseRequestHeadersAccess,
read: UseRequestHeadersAccess,
update: UseRequestHeadersAccess,
delete: UseRequestHeadersAccess,
},
fields: [
{
@@ -396,10 +336,6 @@ export default buildConfigWithDefaults({
},
{
slug: docLevelAccessSlug,
labels: {
singular: 'Doc Level Access',
plural: 'Doc Level Access',
},
access: {
delete: () => ({
and: [
@@ -420,7 +356,6 @@ export default buildConfigWithDefaults({
{
name: 'approvedTitle',
type: 'text',
localized: true,
access: {
update: (args) => {
if (args?.doc?.lockTitle) {
@@ -429,6 +364,7 @@ export default buildConfigWithDefaults({
return true
},
},
localized: true,
},
{
name: 'lockTitle',
@@ -436,6 +372,10 @@ export default buildConfigWithDefaults({
defaultValue: false,
},
],
labels: {
plural: 'Doc Level Access',
singular: 'Doc Level Access',
},
},
{
slug: hiddenFieldsSlug,
@@ -536,6 +476,74 @@ export default buildConfigWithDefaults({
},
Disabled,
],
globals: [
{
slug: 'settings',
admin: {
components: {
elements: {
SaveButton: TestButton,
},
},
},
fields: [
{
name: 'test',
type: 'checkbox',
label: 'Allow access to test global',
},
],
},
{
slug: 'test',
access: {
read: async ({ req: { payload } }) => {
const access = await payload.findGlobal({ slug: 'settings' })
return Boolean(access.test)
},
},
fields: [],
},
{
slug: readOnlyGlobalSlug,
access: {
read: () => true,
update: () => false,
},
fields: [
{
name: 'name',
type: 'text',
},
],
},
{
slug: userRestrictedGlobalSlug,
access: {
read: () => true,
update: ({ data, req }) => data?.name === req.user?.email,
},
fields: [
{
name: 'name',
type: 'text',
},
],
},
{
slug: readNotUpdateGlobalSlug,
access: {
read: () => true,
update: () => false,
},
fields: [
{
name: 'name',
type: 'text',
},
],
},
],
onInit: async (payload) => {
await payload.create({
collection: 'users',
@@ -587,12 +595,12 @@ export default buildConfigWithDefaults({
data: {
array: [
{
text: firstArrayText,
allowPublicReadability: true,
text: firstArrayText,
},
{
text: secondArrayText,
allowPublicReadability: false,
text: secondArrayText,
},
],
},

View File

@@ -422,9 +422,9 @@ describe('access control', () => {
existingDoc = await payload.create({
collection: docLevelAccessSlug,
data: {
approvedForRemoval: false,
approvedTitle: 'Title',
lockTitle: true,
approvedForRemoval: false,
},
})
})
@@ -466,12 +466,12 @@ describe('access control', () => {
await page.waitForURL(logoutURL)
await login({
page,
serverURL,
data: {
email: noAdminAccessEmail,
password: 'test',
},
page,
serverURL,
})
await expect(page.locator('.next-error-h1')).toBeVisible()
@@ -481,12 +481,12 @@ describe('access control', () => {
// Log back in for the next test
await login({
page,
serverURL,
data: {
email: devUser.email,
password: devUser.password,
},
page,
serverURL,
})
})
@@ -500,9 +500,9 @@ describe('access control', () => {
await page.goto(logoutURL)
await page.waitForURL(logoutURL)
const nonAdminUser: NonAdminUser & {
const nonAdminUser: {
token?: string
} = await payload.login({
} & NonAdminUser = await payload.login({
collection: nonAdminUserSlug,
data: {
email: nonAdminUserEmail,
@@ -513,8 +513,8 @@ describe('access control', () => {
await context.addCookies([
{
name: 'payload-token',
value: nonAdminUser.token,
url: serverURL,
value: nonAdminUser.token,
},
])
@@ -554,10 +554,9 @@ describe('access control', () => {
})
})
// eslint-disable-next-line @typescript-eslint/require-await
async function createDoc(data: any): Promise<TypeWithID & Record<string, unknown>> {
async function createDoc(data: any): Promise<Record<string, unknown> & TypeWithID> {
return payload.create({
collection: slug,
data,
}) as any as Promise<TypeWithID & Record<string, unknown>>
}) as any as Promise<Record<string, unknown> & TypeWithID>
}

View File

@@ -0,0 +1,20 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @type {FlatConfig[]} */
export const index = [
...rootEslintConfig,
{
languageOptions: {
parserOptions: {
project: './tsconfig.eslint.json',
tsconfigDirName: import.meta.dirname,
...rootParserOptions,
},
},
},
]
export default index

View File

@@ -1,8 +1,14 @@
import type { Payload, PayloadRequest } from 'payload'
import type {
CollectionSlug,
DataFromCollectionSlug,
Payload,
PayloadRequest,
RequiredDataFromCollectionSlug,
} from 'payload'
import { Forbidden } from 'payload'
import type { Post, RelyOnRequestHeader, Restricted } from './payload-types.js'
import type { FullyRestricted, Post } from './payload-types.js'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
import configPromise, { requestHeaders } from './config.js'
@@ -23,7 +29,7 @@ let payload: Payload
describe('Access Control', () => {
let post1: Post
let restricted: Restricted
let restricted: FullyRestricted
beforeAll(async () => {
;({ payload } = await initPayloadInt(configPromise))
@@ -32,7 +38,7 @@ describe('Access Control', () => {
beforeEach(async () => {
post1 = await payload.create({
collection: slug,
data: { name: 'name' },
data: {},
})
restricted = await payload.create({
@@ -65,21 +71,21 @@ describe('Access Control', () => {
})
await payload.update({
collection: hiddenFieldsSlug,
id: doc.id,
collection: hiddenFieldsSlug,
data: {
title: 'Doc Title',
},
})
const updatedDoc = await payload.findByID({
collection: hiddenFieldsSlug,
id: doc.id,
collection: hiddenFieldsSlug,
showHiddenFields: true,
})
expect(updatedDoc.partiallyHiddenGroup.value).toEqual('private_value')
expect(updatedDoc.partiallyHiddenArray[0].value).toEqual('private_value')
expect(updatedDoc.partiallyHiddenGroup.value).toStrictEqual('private_value')
expect(updatedDoc.partiallyHiddenArray[0].value).toStrictEqual('private_value')
})
it('should not affect hidden fields when patching data - update many', async () => {
@@ -101,22 +107,22 @@ describe('Access Control', () => {
await payload.update({
collection: hiddenFieldsSlug,
where: {
id: { equals: docsMany.id },
},
data: {
title: 'Doc Title',
},
where: {
id: { equals: docsMany.id },
},
})
const updatedMany = await payload.findByID({
collection: hiddenFieldsSlug,
id: docsMany.id,
collection: hiddenFieldsSlug,
showHiddenFields: true,
})
expect(updatedMany.partiallyHiddenGroup.value).toEqual('private_value')
expect(updatedMany.partiallyHiddenArray[0].value).toEqual('private_value')
expect(updatedMany.partiallyHiddenGroup.value).toStrictEqual('private_value')
expect(updatedMany.partiallyHiddenArray[0].value).toStrictEqual('private_value')
})
it('should be able to restrict access based upon siblingData', async () => {
@@ -125,12 +131,12 @@ describe('Access Control', () => {
data: {
array: [
{
text: firstArrayText,
allowPublicReadability: true,
text: firstArrayText,
},
{
text: secondArrayText,
allowPublicReadability: false,
text: secondArrayText,
},
],
},
@@ -159,27 +165,27 @@ describe('Access Control', () => {
describe('Collections', () => {
describe('restricted collection', () => {
it('field without read access should not show', async () => {
const { id } = await createDoc<Post>({ restrictedField: 'restricted' })
const { id } = await createDoc({ restrictedField: 'restricted' })
const retrievedDoc = await payload.findByID({ collection: slug, id, overrideAccess: false })
const retrievedDoc = await payload.findByID({ id, collection: slug, overrideAccess: false })
expect(retrievedDoc.restrictedField).toBeUndefined()
})
it('field without read access should not show when overrideAccess: true', async () => {
const { id, restrictedField } = await createDoc<Post>({ restrictedField: 'restricted' })
const { id, restrictedField } = await createDoc({ restrictedField: 'restricted' })
const retrievedDoc = await payload.findByID({ collection: slug, id, overrideAccess: true })
const retrievedDoc = await payload.findByID({ id, collection: slug, overrideAccess: true })
expect(retrievedDoc.restrictedField).toEqual(restrictedField)
expect(retrievedDoc.restrictedField).toStrictEqual(restrictedField)
})
it('field without read access should not show when overrideAccess default', async () => {
const { id, restrictedField } = await createDoc<Post>({ restrictedField: 'restricted' })
const { id, restrictedField } = await createDoc({ restrictedField: 'restricted' })
const retrievedDoc = await payload.findByID({ collection: slug, id })
const retrievedDoc = await payload.findByID({ id, collection: slug })
expect(retrievedDoc.restrictedField).toEqual(restrictedField)
expect(retrievedDoc.restrictedField).toStrictEqual(restrictedField)
})
})
describe('non-enumerated request properties passed to access control', () => {
@@ -190,25 +196,25 @@ describe('Access Control', () => {
const name = 'name'
const overrideAccess = false
const { id } = await createDoc<RelyOnRequestHeader>({ name }, relyOnRequestHeadersSlug, {
req,
const { id } = await createDoc({ name }, relyOnRequestHeadersSlug, {
overrideAccess,
req,
})
const docById = await payload.findByID({
collection: relyOnRequestHeadersSlug,
id,
req,
collection: relyOnRequestHeadersSlug,
overrideAccess,
req,
})
const { docs: docsByName } = await payload.find({
collection: relyOnRequestHeadersSlug,
overrideAccess,
req,
where: {
name: {
equals: name,
},
},
req,
overrideAccess,
})
expect(docById).not.toBeUndefined()
@@ -220,23 +226,25 @@ describe('Access Control', () => {
const overrideAccess = false
await expect(() =>
createDoc<RelyOnRequestHeader>({ name }, relyOnRequestHeadersSlug, { overrideAccess }),
createDoc({ name }, relyOnRequestHeadersSlug, {
overrideAccess,
}),
).rejects.toThrow(Forbidden)
const { id } = await createDoc<RelyOnRequestHeader>({ name }, relyOnRequestHeadersSlug)
const { id } = await createDoc({ name }, relyOnRequestHeadersSlug)
await expect(() =>
payload.findByID({ collection: relyOnRequestHeadersSlug, id, overrideAccess }),
payload.findByID({ id, collection: relyOnRequestHeadersSlug, overrideAccess }),
).rejects.toThrow(Forbidden)
await expect(() =>
payload.find({
collection: relyOnRequestHeadersSlug,
overrideAccess,
where: {
name: {
equals: name,
},
},
overrideAccess,
}),
).rejects.toThrow(Forbidden)
})
@@ -248,8 +256,8 @@ describe('Access Control', () => {
it('should allow overrideAccess: false', async () => {
const req = async () =>
await payload.update({
collection: slug,
id: post1.id,
collection: slug,
data: { restrictedField: restricted.id },
overrideAccess: false, // this should respect access control
})
@@ -259,8 +267,8 @@ describe('Access Control', () => {
it('should allow overrideAccess: true', async () => {
const doc = await payload.update({
collection: slug,
id: post1.id,
collection: slug,
data: { restrictedField: restricted.id },
overrideAccess: true, // this should override access control
})
@@ -270,8 +278,8 @@ describe('Access Control', () => {
it('should allow overrideAccess by default', async () => {
const doc = await payload.update({
collection: slug,
id: post1.id,
collection: slug,
data: { restrictedField: restricted.id },
})
@@ -282,11 +290,11 @@ describe('Access Control', () => {
const req = async () =>
await payload.update({
collection: slug,
data: { restrictedField: restricted.id },
overrideAccess: false, // this should respect access control
where: {
id: { equals: post1.id },
},
data: { restrictedField: restricted.id },
overrideAccess: false, // this should respect access control
})
await expect(req).rejects.toThrow(Forbidden)
@@ -295,11 +303,11 @@ describe('Access Control', () => {
it('should allow overrideAccess: true - update many', async () => {
const doc = await payload.update({
collection: slug,
data: { restrictedField: restricted.id },
overrideAccess: true, // this should override access control
where: {
id: { equals: post1.id },
},
data: { restrictedField: restricted.id },
overrideAccess: true, // this should override access control
})
expect(doc.docs[0]).toMatchObject({ id: post1.id })
@@ -308,10 +316,10 @@ describe('Access Control', () => {
it('should allow overrideAccess by default - update many', async () => {
const doc = await payload.update({
collection: slug,
data: { restrictedField: restricted.id },
where: {
id: { equals: post1.id },
},
data: { restrictedField: restricted.id },
})
expect(doc.docs[0]).toMatchObject({ id: post1.id })
@@ -324,8 +332,8 @@ describe('Access Control', () => {
it('should allow overrideAccess: false', async () => {
const req = async () =>
await payload.update({
collection: fullyRestrictedSlug,
id: restricted.id,
collection: fullyRestrictedSlug,
data: { name: updatedName },
overrideAccess: false, // this should respect access control
})
@@ -335,8 +343,8 @@ describe('Access Control', () => {
it('should allow overrideAccess: true', async () => {
const doc = await payload.update({
collection: fullyRestrictedSlug,
id: restricted.id,
collection: fullyRestrictedSlug,
data: { name: updatedName },
overrideAccess: true, // this should override access control
})
@@ -346,8 +354,8 @@ describe('Access Control', () => {
it('should allow overrideAccess by default', async () => {
const doc = await payload.update({
collection: fullyRestrictedSlug,
id: restricted.id,
collection: fullyRestrictedSlug,
data: { name: updatedName },
})
@@ -358,11 +366,11 @@ describe('Access Control', () => {
const req = async () =>
await payload.update({
collection: fullyRestrictedSlug,
data: { name: updatedName },
overrideAccess: false, // this should respect access control
where: {
id: { equals: restricted.id },
},
data: { name: updatedName },
overrideAccess: false, // this should respect access control
})
await expect(req).rejects.toThrow(Forbidden)
@@ -371,11 +379,11 @@ describe('Access Control', () => {
it('should allow overrideAccess: true - update many', async () => {
const doc = await payload.update({
collection: fullyRestrictedSlug,
data: { name: updatedName },
overrideAccess: true, // this should override access control
where: {
id: { equals: restricted.id },
},
data: { name: updatedName },
overrideAccess: true, // this should override access control
})
expect(doc.docs[0]).toMatchObject({ id: restricted.id, name: updatedName })
@@ -384,10 +392,10 @@ describe('Access Control', () => {
it('should allow overrideAccess by default - update many', async () => {
const doc = await payload.update({
collection: fullyRestrictedSlug,
data: { name: updatedName },
where: {
id: { equals: restricted.id },
},
data: { name: updatedName },
})
expect(doc.docs[0]).toMatchObject({ id: restricted.id, name: updatedName })
@@ -407,8 +415,8 @@ describe('Access Control', () => {
await payload.create({
collection: hiddenAccessSlug,
data: {
title: 'hello',
hidden: true,
title: 'hello',
},
})
@@ -431,8 +439,8 @@ describe('Access Control', () => {
await payload.create({
collection: hiddenAccessCountSlug,
data: {
title: 'hello',
hidden: true,
title: 'hello',
},
})
@@ -461,11 +469,11 @@ describe('Access Control', () => {
},
})
const { docs } = await payload.findVersions({
collection: restrictedVersionsSlug,
overrideAccess: false,
where: {
'version.name': { equals: 'match' },
},
collection: restrictedVersionsSlug,
overrideAccess: false,
})
expect(docs).toHaveLength(1)
@@ -473,14 +481,16 @@ describe('Access Control', () => {
})
})
async function createDoc<Collection>(
data: Partial<Collection>,
overrideSlug = slug,
async function createDoc<TSlug extends CollectionSlug = 'posts'>(
data: RequiredDataFromCollectionSlug<TSlug>,
overrideSlug?: TSlug,
options?: Partial<Parameters<Payload['create']>[0]>,
) {
): Promise<DataFromCollectionSlug<TSlug>> {
// @ts-expect-error
return await payload.create({
...options,
collection: overrideSlug,
collection: overrideSlug ?? slug,
// @ts-expect-error
data: data ?? {},
})
}