Merge branch 'feat/1.0' of github.com:payloadcms/payload into feat/1.0
This commit is contained in:
42
test/dev/generateTypes.js
Normal file
42
test/dev/generateTypes.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const babelConfig = require('../../babel.config');
|
||||
|
||||
require('@babel/register')({
|
||||
...babelConfig,
|
||||
extensions: ['.ts', '.tsx', '.js', '.jsx'],
|
||||
env: {
|
||||
development: {
|
||||
sourceMaps: 'inline',
|
||||
retainLines: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { generateTypes } = require('../../src/bin/generateTypes');
|
||||
|
||||
const [testConfigDir] = process.argv.slice(2);
|
||||
const testDir = path.resolve(__dirname, '../', testConfigDir);
|
||||
|
||||
// Generate types for entire directory
|
||||
if (testConfigDir === 'int' || testConfigDir === 'e2e') {
|
||||
fs.readdirSync(testDir, { withFileTypes: true })
|
||||
.filter((f) => f.isDirectory())
|
||||
.forEach((dir) => {
|
||||
const suiteDir = path.resolve(testDir, dir.name);
|
||||
setPaths(suiteDir);
|
||||
generateTypes();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate for specific test suite directory
|
||||
setPaths(testDir);
|
||||
generateTypes();
|
||||
|
||||
// Set config path and TS output path using test dir
|
||||
function setPaths(dir) {
|
||||
const configPath = path.resolve(dir, 'config.ts');
|
||||
process.env.PAYLOAD_CONFIG_PATH = configPath;
|
||||
process.env.PAYLOAD_TS_OUTPUT_PATH = path.resolve(dir, 'payload-types.ts');
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
/* eslint-disable no-console */
|
||||
import express from 'express';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import payload from '../../src';
|
||||
|
||||
const expressApp = express();
|
||||
|
||||
const init = async () => {
|
||||
await payload.init({
|
||||
secret: 'SECRET_KEY',
|
||||
secret: uuid(),
|
||||
mongoURL: process.env.MONGO_URL || 'mongodb://localhost/payload',
|
||||
express: expressApp,
|
||||
email: {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { devUser } from '../../credentials';
|
||||
import { buildConfig } from '../buildConfig';
|
||||
import type { ReadOnlyCollection } from './payload-types';
|
||||
|
||||
export const slug = 'access-controls';
|
||||
export const readOnlySlug = 'read-only-collection';
|
||||
export const restrictedSlug = 'restricted';
|
||||
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
@@ -17,20 +21,52 @@ export default buildConfig({
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'restricted',
|
||||
slug: restrictedSlug,
|
||||
fields: [],
|
||||
access: {
|
||||
create: () => false,
|
||||
read: () => false,
|
||||
update: () => false,
|
||||
delete: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: readOnlySlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
access: {
|
||||
create: () => false,
|
||||
read: () => true,
|
||||
update: () => false,
|
||||
delete: () => false,
|
||||
},
|
||||
},
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
});
|
||||
|
||||
await payload.create({
|
||||
collection: slug,
|
||||
data: {
|
||||
restrictedField: 'restricted',
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
await payload.create<ReadOnlyCollection>({
|
||||
collection: readOnlySlug,
|
||||
data: {
|
||||
name: 'read-only',
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,21 +3,13 @@ import { expect, test } from '@playwright/test';
|
||||
import payload from '../../../src';
|
||||
import { AdminUrlUtil } from '../../helpers/adminUrlUtil';
|
||||
import { initPayloadE2E } from '../../helpers/configHelpers';
|
||||
import { firstRegister } from '../helpers';
|
||||
import { slug } from './config';
|
||||
import { login } from '../helpers';
|
||||
import { readOnlySlug, restrictedSlug, slug } from './config';
|
||||
import type { ReadOnlyCollection } from './payload-types';
|
||||
|
||||
/**
|
||||
* TODO: Access Control
|
||||
* - [x] restricted collections not shown
|
||||
* - no sidebar link
|
||||
* - no route
|
||||
* - no card
|
||||
* [x] field without read access should not show
|
||||
* prevent user from logging in (canAccessAdmin)
|
||||
* no create controls if no access
|
||||
* no update control if no update
|
||||
* - check fields are rendered as readonly
|
||||
* no delete control if no access
|
||||
* no version controls is no access
|
||||
*
|
||||
* FSK: 'should properly prevent / allow public users from reading a restricted field'
|
||||
@@ -26,26 +18,26 @@ import { slug } from './config';
|
||||
*/
|
||||
|
||||
const { beforeAll, describe } = test;
|
||||
let url: AdminUrlUtil;
|
||||
|
||||
describe('access control', () => {
|
||||
let page: Page;
|
||||
let url: AdminUrlUtil;
|
||||
let restrictedUrl: AdminUrlUtil;
|
||||
let readoOnlyUrl: AdminUrlUtil;
|
||||
|
||||
beforeAll(async ({ browser }) => {
|
||||
const { serverURL } = await initPayloadE2E(__dirname);
|
||||
// await clearDocs(); // Clear any seeded data from onInit
|
||||
|
||||
url = new AdminUrlUtil(serverURL, slug);
|
||||
restrictedUrl = new AdminUrlUtil(serverURL, restrictedSlug);
|
||||
readoOnlyUrl = new AdminUrlUtil(serverURL, readOnlySlug);
|
||||
|
||||
const context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
|
||||
await firstRegister({ page, serverURL });
|
||||
await login({ page, serverURL });
|
||||
});
|
||||
|
||||
// afterEach(async () => {
|
||||
// });
|
||||
|
||||
test('field without read access should not show', async () => {
|
||||
const { id } = await createDoc({ restrictedField: 'restricted' });
|
||||
|
||||
@@ -55,9 +47,20 @@ describe('access control', () => {
|
||||
});
|
||||
|
||||
describe('restricted collection', () => {
|
||||
let existingDoc: ReadOnlyCollection;
|
||||
|
||||
beforeAll(async () => {
|
||||
existingDoc = await payload.create<ReadOnlyCollection>({
|
||||
collection: readOnlySlug,
|
||||
data: {
|
||||
name: 'name',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should not show in card list', async () => {
|
||||
await page.goto(url.admin);
|
||||
await expect(page.locator('.dashboard__card-list >> text=Restricteds')).toHaveCount(0);
|
||||
await expect(page.locator(`#card-${restrictedSlug}`)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should not show in nav', async () => {
|
||||
@@ -65,11 +68,67 @@ describe('access control', () => {
|
||||
await expect(page.locator('.nav >> a:has-text("Restricteds")')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should not have collection url', async () => {
|
||||
await page.goto(url.list);
|
||||
await page.locator('text=Nothing found').click();
|
||||
await page.locator('a:has-text("Back to Dashboard")').click();
|
||||
await expect(page).toHaveURL(url.admin);
|
||||
test('should not have list url', async () => {
|
||||
await page.goto(restrictedUrl.list);
|
||||
await expect(page.locator('.unauthorized')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not have create url', async () => {
|
||||
await page.goto(restrictedUrl.create);
|
||||
await expect(page.locator('.unauthorized')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not have access to existing doc', async () => {
|
||||
await page.goto(restrictedUrl.edit(existingDoc.id));
|
||||
await expect(page.locator('.unauthorized')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('read-only collection', () => {
|
||||
let existingDoc: ReadOnlyCollection;
|
||||
|
||||
beforeAll(async () => {
|
||||
existingDoc = await payload.create<ReadOnlyCollection>({
|
||||
collection: readOnlySlug,
|
||||
data: {
|
||||
name: 'name',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should show in card list', async () => {
|
||||
await page.goto(url.admin);
|
||||
await expect(page.locator(`#card-${readOnlySlug}`)).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should show in nav', async () => {
|
||||
await page.goto(url.admin);
|
||||
await expect(page.locator(`.nav a[href="/admin/collections/${readOnlySlug}"]`)).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should have collection url', async () => {
|
||||
await page.goto(readoOnlyUrl.list);
|
||||
await expect(page).toHaveURL(readoOnlyUrl.list); // no redirect
|
||||
});
|
||||
|
||||
test('should not have "Create New" button', async () => {
|
||||
await page.goto(readoOnlyUrl.create);
|
||||
await expect(page.locator('.collection-list__header a')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should not have quick create button', async () => {
|
||||
await page.goto(url.admin);
|
||||
await expect(page.locator(`#card-${readOnlySlug}`)).not.toHaveClass('card__actions');
|
||||
});
|
||||
|
||||
test('edit view should not have buttons', async () => {
|
||||
await page.goto(readoOnlyUrl.edit(existingDoc.id));
|
||||
await expect(page.locator('.collection-edit__collection-actions li')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('fields should be read-only', async () => {
|
||||
await page.goto(readoOnlyUrl.edit(existingDoc.id));
|
||||
await expect(page.locator('#field-name')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
51
test/e2e/access-control/payload-types.ts
Normal file
51
test/e2e/access-control/payload-types.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload CMS.
|
||||
* 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 {}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "access-controls".
|
||||
*/
|
||||
export interface AccessControl {
|
||||
id: string;
|
||||
restrictedField?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "restricted".
|
||||
*/
|
||||
export interface Restricted {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "read-only-collection".
|
||||
*/
|
||||
export interface ReadOnlyCollection {
|
||||
id: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
27
test/e2e/auth/payload-types.ts
Normal file
27
test/e2e/auth/payload-types.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload CMS.
|
||||
* 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 {}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
enableAPIKey?: boolean;
|
||||
apiKey?: string;
|
||||
apiKeyIndex?: string;
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
_verified?: boolean;
|
||||
_verificationToken?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -3,7 +3,11 @@ import { buildConfig as buildPayloadConfig } from '../../src/config/build';
|
||||
import type { Config, SanitizedConfig } from '../../src/config/types';
|
||||
|
||||
export function buildConfig(overrides?: Partial<Config>): SanitizedConfig {
|
||||
const baseConfig: Config = {};
|
||||
const baseConfig: Config = {
|
||||
typescript: {
|
||||
outputFile: process.env.PAYLOAD_TS_OUTPUT_PATH,
|
||||
},
|
||||
};
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
baseConfig.admin = {
|
||||
webpack: (config) => ({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mapAsync } from '../../../src/utilities/mapAsync';
|
||||
import { devUser } from '../../credentials';
|
||||
import { buildConfig } from '../buildConfig';
|
||||
|
||||
export const slug = 'posts';
|
||||
@@ -26,6 +27,14 @@ export default buildConfig({
|
||||
],
|
||||
}],
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
});
|
||||
|
||||
await mapAsync([...Array(11)], async () => {
|
||||
await payload.create({
|
||||
collection: slug,
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import payload from '../../../src';
|
||||
import { AdminUrlUtil } from '../../helpers/adminUrlUtil';
|
||||
import { initPayloadTest } from '../../helpers/configHelpers';
|
||||
import { firstRegister, saveDocAndAssert } from '../helpers';
|
||||
import { initPayloadE2E } from '../../helpers/configHelpers';
|
||||
import { login, saveDocAndAssert } from '../helpers';
|
||||
import type { Post } from './config';
|
||||
import { slug } from './config';
|
||||
import { mapAsync } from '../../../src/utilities/mapAsync';
|
||||
@@ -20,19 +20,14 @@ describe('collections', () => {
|
||||
let page: Page;
|
||||
|
||||
beforeAll(async ({ browser }) => {
|
||||
const { serverURL } = await initPayloadTest({
|
||||
__dirname,
|
||||
init: {
|
||||
local: false,
|
||||
},
|
||||
});
|
||||
const { serverURL } = await initPayloadE2E(__dirname);
|
||||
await clearDocs(); // Clear any seeded data from onInit
|
||||
url = new AdminUrlUtil(serverURL, slug);
|
||||
|
||||
const context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
|
||||
await firstRegister({ page, serverURL });
|
||||
await login({ page, serverURL });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -50,13 +45,13 @@ describe('collections', () => {
|
||||
|
||||
test('should navigate to collection - card', async () => {
|
||||
await page.goto(url.admin);
|
||||
await page.locator('a:has-text("Posts")').click();
|
||||
await page.locator(`#card-${slug}`).click();
|
||||
expect(page.url()).toContain(url.list);
|
||||
});
|
||||
|
||||
test('breadcrumbs - from card to dashboard', async () => {
|
||||
test('breadcrumbs - from list to dashboard', async () => {
|
||||
await page.goto(url.list);
|
||||
await page.locator('a:has-text("Dashboard")').click();
|
||||
await page.locator('.step-nav a[href="/admin"]').click();
|
||||
expect(page.url()).toContain(url.admin);
|
||||
});
|
||||
|
||||
@@ -64,7 +59,7 @@ describe('collections', () => {
|
||||
const { id } = await createPost();
|
||||
|
||||
await page.goto(url.edit(id));
|
||||
await page.locator('nav >> text=Posts').click();
|
||||
await page.locator(`.step-nav >> text=${slug}`).click();
|
||||
expect(page.url()).toContain(url.list);
|
||||
});
|
||||
});
|
||||
@@ -73,14 +68,14 @@ describe('collections', () => {
|
||||
describe('CRUD', () => {
|
||||
test('should create', async () => {
|
||||
await page.goto(url.create);
|
||||
await page.locator('#title').fill(title);
|
||||
await page.locator('#description').fill(description);
|
||||
await page.click('text=Save', { delay: 100 });
|
||||
await page.locator('#field-title').fill(title);
|
||||
await page.locator('#field-description').fill(description);
|
||||
await page.click('#action-save', { delay: 100 });
|
||||
|
||||
await saveDocAndAssert(page);
|
||||
|
||||
await expect(page.locator('#title')).toHaveValue(title);
|
||||
await expect(page.locator('#description')).toHaveValue(description);
|
||||
await expect(page.locator('#field-title')).toHaveValue(title);
|
||||
await expect(page.locator('#field-description')).toHaveValue(description);
|
||||
});
|
||||
|
||||
test('should read existing', async () => {
|
||||
@@ -88,8 +83,8 @@ describe('collections', () => {
|
||||
|
||||
await page.goto(url.edit(id));
|
||||
|
||||
await expect(page.locator('#title')).toHaveValue(title);
|
||||
await expect(page.locator('#description')).toHaveValue(description);
|
||||
await expect(page.locator('#field-title')).toHaveValue(title);
|
||||
await expect(page.locator('#field-description')).toHaveValue(description);
|
||||
});
|
||||
|
||||
test('should update existing', async () => {
|
||||
@@ -99,21 +94,21 @@ describe('collections', () => {
|
||||
|
||||
const newTitle = 'new title';
|
||||
const newDesc = 'new description';
|
||||
await page.locator('#title').fill(newTitle);
|
||||
await page.locator('#description').fill(newDesc);
|
||||
await page.locator('#field-title').fill(newTitle);
|
||||
await page.locator('#field-description').fill(newDesc);
|
||||
|
||||
await saveDocAndAssert(page);
|
||||
|
||||
await expect(page.locator('#title')).toHaveValue(newTitle);
|
||||
await expect(page.locator('#description')).toHaveValue(newDesc);
|
||||
await expect(page.locator('#field-title')).toHaveValue(newTitle);
|
||||
await expect(page.locator('#field-description')).toHaveValue(newDesc);
|
||||
});
|
||||
|
||||
test('should delete existing', async () => {
|
||||
const { id } = await createPost();
|
||||
|
||||
await page.goto(url.edit(id));
|
||||
await page.locator('button:has-text("Delete")').click();
|
||||
await page.locator('button:has-text("Confirm")').click();
|
||||
await page.locator('#action-delete').click();
|
||||
await page.locator('#confirm-delete').click();
|
||||
|
||||
await expect(page.locator(`text=Post "${id}" successfully deleted.`)).toBeVisible();
|
||||
expect(page.url()).toContain(url.list);
|
||||
@@ -123,10 +118,10 @@ describe('collections', () => {
|
||||
const { id } = await createPost();
|
||||
|
||||
await page.goto(url.edit(id));
|
||||
await page.locator('button:has-text("Duplicate")').click();
|
||||
await page.locator('#action-duplicate').click();
|
||||
|
||||
expect(page.url()).toContain(url.create);
|
||||
await page.locator('button:has-text("Save")').click();
|
||||
await page.locator('#action-save').click();
|
||||
expect(page.url()).not.toContain(id); // new id
|
||||
});
|
||||
});
|
||||
@@ -149,7 +144,7 @@ describe('collections', () => {
|
||||
test('toggle columns', async () => {
|
||||
const columnCountLocator = 'table >> thead >> tr >> th';
|
||||
await createPost();
|
||||
await page.locator('button:has-text("Columns")').click();
|
||||
await page.locator('.list-controls__toggle-columns').click();
|
||||
await wait(1000); // Wait for column toggle UI, should probably use waitForSelector
|
||||
|
||||
const numberOfColumns = await page.locator(columnCountLocator).count();
|
||||
@@ -170,13 +165,13 @@ describe('collections', () => {
|
||||
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(2);
|
||||
|
||||
await page.locator('button:has-text("Filters")').click();
|
||||
await page.locator('.list-controls__toggle-where').click();
|
||||
await wait(1000); // Wait for column toggle UI, should probably use waitForSelector
|
||||
|
||||
await page.locator('text=Add filter').click();
|
||||
await page.locator('.where-builder__add-first-filter').click();
|
||||
|
||||
const operatorField = page.locator('.condition >> .condition__operator');
|
||||
const valueField = page.locator('.condition >> .condition__value >> input');
|
||||
const operatorField = page.locator('.condition__operator');
|
||||
const valueField = page.locator('.condition__value >> input');
|
||||
|
||||
await operatorField.click();
|
||||
|
||||
@@ -192,7 +187,7 @@ describe('collections', () => {
|
||||
expect(firstId).toEqual(id);
|
||||
|
||||
// Remove filter
|
||||
await page.locator('.condition >> .icon--x').click();
|
||||
await page.locator('.condition__actions-remove').click();
|
||||
await wait(1000);
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(2);
|
||||
});
|
||||
@@ -237,14 +232,11 @@ describe('collections', () => {
|
||||
|
||||
await expect(getTableItems()).toHaveCount(2);
|
||||
|
||||
const chevrons = page.locator('table >> thead >> th >> button');
|
||||
const upChevron = chevrons.first();
|
||||
const downChevron = chevrons.nth(1);
|
||||
const upChevron = page.locator('#heading-id .sort-column__asc');
|
||||
const downChevron = page.locator('#heading-id .sort-column__desc');
|
||||
|
||||
const getFirstId = async () => getTableItems().first().locator('td').first()
|
||||
.innerText();
|
||||
const getSecondId = async () => getTableItems().nth(1).locator('td').first()
|
||||
.innerText();
|
||||
const getFirstId = async () => page.locator('.row-1 .cell-id').innerText();
|
||||
const getSecondId = async () => page.locator('.row-2 .cell-id').innerText();
|
||||
|
||||
const firstId = await getFirstId();
|
||||
const secondId = await getSecondId();
|
||||
|
||||
33
test/e2e/collections/payload-types.ts
Normal file
33
test/e2e/collections/payload-types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload CMS.
|
||||
* 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 {}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
35
test/e2e/fields-array/payload-types.ts
Normal file
35
test/e2e/fields-array/payload-types.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload CMS.
|
||||
* 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 {}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "fields-array".
|
||||
*/
|
||||
export interface FieldsArray {
|
||||
id: string;
|
||||
readOnlyArray?: {
|
||||
text?: string;
|
||||
id?: string;
|
||||
}[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { CollectionConfig } from '../../../src/collections/config/types';
|
||||
import { buildConfig } from '../buildConfig';
|
||||
import { devUser } from '../../credentials';
|
||||
import { mapAsync } from '../../../src/utilities/mapAsync';
|
||||
|
||||
export const slug = 'fields-relationship';
|
||||
|
||||
@@ -12,6 +14,7 @@ export interface FieldsRelationship {
|
||||
id: string;
|
||||
relationship: RelationOne;
|
||||
relationshipHasMany: RelationOne[];
|
||||
relationshipHasManyMultiple: Array<RelationOne | RelationTwo | { relationTo: string, value: string}>;
|
||||
relationshipMultiple: Array<RelationOne | RelationTwo>;
|
||||
relationshipRestricted: RelationRestricted;
|
||||
relationshipWithTitle: RelationWithTitle;
|
||||
@@ -38,6 +41,9 @@ export default buildConfig({
|
||||
collections: [
|
||||
{
|
||||
slug,
|
||||
admin: {
|
||||
defaultColumns: ['id', 'relationship', 'relationshipRestricted', 'relationshipHasManyMultiple', 'relationshipWithTitle'],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'relationship',
|
||||
@@ -55,6 +61,12 @@ export default buildConfig({
|
||||
name: 'relationshipMultiple',
|
||||
relationTo: [relationOneSlug, relationTwoSlug],
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'relationshipHasManyMultiple',
|
||||
hasMany: true,
|
||||
relationTo: [relationOneSlug, relationTwoSlug],
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'relationshipRestricted',
|
||||
@@ -94,6 +106,13 @@ export default buildConfig({
|
||||
},
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
});
|
||||
// Create docs to relate to
|
||||
const { id: relationOneDocId } = await payload.create<RelationOne>({
|
||||
collection: relationOneSlug,
|
||||
@@ -102,18 +121,26 @@ export default buildConfig({
|
||||
},
|
||||
});
|
||||
|
||||
await payload.create<RelationOne>({
|
||||
collection: relationOneSlug,
|
||||
data: {
|
||||
name: relationOneSlug,
|
||||
},
|
||||
const relationOneIDs = [];
|
||||
await mapAsync([...Array(5)], async () => {
|
||||
const doc = await payload.create<RelationOne>({
|
||||
collection: relationOneSlug,
|
||||
data: {
|
||||
name: relationOneSlug,
|
||||
},
|
||||
});
|
||||
relationOneIDs.push(doc.id);
|
||||
});
|
||||
|
||||
await payload.create<RelationTwo>({
|
||||
collection: relationTwoSlug,
|
||||
data: {
|
||||
name: relationTwoSlug,
|
||||
},
|
||||
const relationTwoIDs = [];
|
||||
await mapAsync([...Array(11)], async () => {
|
||||
const doc = await payload.create<RelationTwo>({
|
||||
collection: relationTwoSlug,
|
||||
data: {
|
||||
name: relationTwoSlug,
|
||||
},
|
||||
});
|
||||
relationTwoIDs.push(doc.id);
|
||||
});
|
||||
|
||||
// Existing relationships
|
||||
@@ -129,14 +156,36 @@ export default buildConfig({
|
||||
name: 'relation-title',
|
||||
},
|
||||
});
|
||||
await payload.create<RelationOne>({
|
||||
await payload.create<FieldsRelationship>({
|
||||
collection: slug,
|
||||
data: {
|
||||
name: 'with-existing-relations',
|
||||
relationship: relationOneDocId,
|
||||
relationshipRestricted: restrictedDocId,
|
||||
relationshipWithTitle: relationWithTitleDocId,
|
||||
},
|
||||
});
|
||||
await mapAsync([...Array(11)], async () => {
|
||||
await payload.create<FieldsRelationship>({
|
||||
collection: slug,
|
||||
data: {
|
||||
relationship: relationOneDocId,
|
||||
relationshipRestricted: restrictedDocId,
|
||||
relationshipHasManyMultiple: relationOneIDs.map((id) => ({ relationTo: relationOneSlug, value: id })),
|
||||
},
|
||||
});
|
||||
});
|
||||
await mapAsync([...Array(15)], async () => {
|
||||
const relationOneID = relationOneIDs[Math.floor(Math.random() * 10)];
|
||||
const relationTwoID = relationTwoIDs[Math.floor(Math.random() * 10)];
|
||||
await payload.create<FieldsRelationship>({
|
||||
collection: slug,
|
||||
data: {
|
||||
relationship: relationOneDocId,
|
||||
relationshipRestricted: restrictedDocId,
|
||||
relationshipHasMany: [relationOneID],
|
||||
relationshipHasManyMultiple: [{ relationTo: relationTwoSlug, value: relationTwoID }],
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@ import { expect, test } from '@playwright/test';
|
||||
import payload from '../../../src';
|
||||
import { mapAsync } from '../../../src/utilities/mapAsync';
|
||||
import { AdminUrlUtil } from '../../helpers/adminUrlUtil';
|
||||
import { initPayloadTest } from '../../helpers/configHelpers';
|
||||
import { firstRegister, saveDocAndAssert } from '../helpers';
|
||||
import { initPayloadE2E, initPayloadTest } from '../../helpers/configHelpers';
|
||||
import { login, saveDocAndAssert } from '../helpers';
|
||||
import type {
|
||||
FieldsRelationship as CollectionWithRelationships,
|
||||
RelationOne,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
relationWithTitleSlug,
|
||||
slug,
|
||||
} from './config';
|
||||
import wait from '../../../src/utilities/wait';
|
||||
|
||||
const { beforeAll, describe } = test;
|
||||
|
||||
@@ -35,12 +36,7 @@ describe('fields - relationship', () => {
|
||||
let relationWithTitle: RelationWithTitle;
|
||||
|
||||
beforeAll(async ({ browser }) => {
|
||||
const { serverURL } = await initPayloadTest({
|
||||
__dirname,
|
||||
init: {
|
||||
local: false,
|
||||
},
|
||||
});
|
||||
const { serverURL } = await initPayloadE2E(__dirname);
|
||||
await clearAllDocs();
|
||||
|
||||
url = new AdminUrlUtil(serverURL, slug);
|
||||
@@ -97,16 +93,15 @@ describe('fields - relationship', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await firstRegister({ page, serverURL });
|
||||
await login({ page, serverURL });
|
||||
});
|
||||
|
||||
test('should create relationship', async () => {
|
||||
await page.goto(url.create);
|
||||
|
||||
const fields = page.locator('.render-fields >> .react-select');
|
||||
const relationshipField = fields.nth(0);
|
||||
const field = page.locator('#field-relationship');
|
||||
|
||||
await relationshipField.click({ delay: 100 });
|
||||
await field.click({ delay: 100 });
|
||||
|
||||
const options = page.locator('.rs__option');
|
||||
|
||||
@@ -114,7 +109,7 @@ describe('fields - relationship', () => {
|
||||
|
||||
// Select a relationship
|
||||
await options.nth(1).click();
|
||||
await expect(relationshipField).toContainText(relationOneDoc.id);
|
||||
await expect(field).toContainText(relationOneDoc.id);
|
||||
|
||||
await saveDocAndAssert(page);
|
||||
});
|
||||
@@ -122,10 +117,9 @@ describe('fields - relationship', () => {
|
||||
test('should create hasMany relationship', async () => {
|
||||
await page.goto(url.create);
|
||||
|
||||
const fields = page.locator('.render-fields >> .react-select');
|
||||
const relationshipHasManyField = fields.nth(1);
|
||||
const field = page.locator('.field-relationshipHasMany');
|
||||
|
||||
await relationshipHasManyField.click({ delay: 100 });
|
||||
await field.click({ delay: 100 });
|
||||
|
||||
const options = page.locator('.rs__option');
|
||||
|
||||
@@ -133,16 +127,16 @@ describe('fields - relationship', () => {
|
||||
|
||||
// Add one relationship
|
||||
await options.locator(`text=${relationOneDoc.id}`).click();
|
||||
await expect(relationshipHasManyField).toContainText(relationOneDoc.id);
|
||||
await expect(relationshipHasManyField).not.toContainText(anotherRelationOneDoc.id);
|
||||
await expect(field).toContainText(relationOneDoc.id);
|
||||
await expect(field).not.toContainText(anotherRelationOneDoc.id);
|
||||
|
||||
// Add second relationship
|
||||
await relationshipHasManyField.click({ delay: 100 });
|
||||
await field.click({ delay: 100 });
|
||||
await options.locator(`text=${anotherRelationOneDoc.id}`).click();
|
||||
await expect(relationshipHasManyField).toContainText(anotherRelationOneDoc.id);
|
||||
await expect(field).toContainText(anotherRelationOneDoc.id);
|
||||
|
||||
// No options left
|
||||
await relationshipHasManyField.click({ delay: 100 });
|
||||
await field.click({ delay: 100 });
|
||||
await expect(page.locator('.rs__menu')).toHaveText('No options');
|
||||
|
||||
await saveDocAndAssert(page);
|
||||
@@ -151,10 +145,9 @@ describe('fields - relationship', () => {
|
||||
test('should create relations to multiple collections', async () => {
|
||||
await page.goto(url.create);
|
||||
|
||||
const fields = page.locator('.render-fields >> .react-select');
|
||||
const relationshipMultipleField = fields.nth(2);
|
||||
const field = page.locator('.field-relationshipMultiple');
|
||||
|
||||
await relationshipMultipleField.click({ delay: 100 });
|
||||
await field.click({ delay: 100 });
|
||||
|
||||
const options = page.locator('.rs__option');
|
||||
|
||||
@@ -162,12 +155,12 @@ describe('fields - relationship', () => {
|
||||
|
||||
// Add one relationship
|
||||
await options.locator(`text=${relationOneDoc.id}`).click();
|
||||
await expect(relationshipMultipleField).toContainText(relationOneDoc.id);
|
||||
await expect(field).toContainText(relationOneDoc.id);
|
||||
|
||||
// Add relationship of different collection
|
||||
await relationshipMultipleField.click({ delay: 100 });
|
||||
await field.click({ delay: 100 });
|
||||
await options.locator(`text=${relationTwoDoc.id}`).click();
|
||||
await expect(relationshipMultipleField).toContainText(relationTwoDoc.id);
|
||||
await expect(field).toContainText(relationTwoDoc.id);
|
||||
|
||||
await saveDocAndAssert(page);
|
||||
});
|
||||
@@ -176,11 +169,10 @@ describe('fields - relationship', () => {
|
||||
test('should highlight existing relationship', async () => {
|
||||
await page.goto(url.edit(docWithExistingRelations.id));
|
||||
|
||||
const fields = page.locator('.render-fields >> .react-select');
|
||||
const relationOneField = fields.nth(0);
|
||||
const field = page.locator('#field-relationship');
|
||||
|
||||
// Check dropdown options
|
||||
await relationOneField.click({ delay: 100 });
|
||||
await field.click({ delay: 100 });
|
||||
|
||||
await expect(page.locator('.rs__option--is-selected')).toHaveCount(1);
|
||||
await expect(page.locator('.rs__option--is-selected')).toHaveText(relationOneDoc.id);
|
||||
@@ -189,33 +181,58 @@ describe('fields - relationship', () => {
|
||||
test('should show untitled ID on restricted relation', async () => {
|
||||
await page.goto(url.edit(docWithExistingRelations.id));
|
||||
|
||||
const fields = page.locator('.render-fields >> .react-select');
|
||||
const restrictedRelationField = fields.nth(3);
|
||||
const field = page.locator('#field-relationshipRestricted');
|
||||
|
||||
// Check existing relationship has untitled ID
|
||||
await expect(restrictedRelationField).toContainText(`Untitled - ID: ${restrictedRelation.id}`);
|
||||
await expect(field).toContainText(`Untitled - ID: ${restrictedRelation.id}`);
|
||||
|
||||
// Check dropdown options
|
||||
await restrictedRelationField.click({ delay: 100 });
|
||||
await field.click({ delay: 100 });
|
||||
const options = page.locator('.rs__option');
|
||||
|
||||
await expect(options).toHaveCount(2); // None + 1 Unitled ID
|
||||
});
|
||||
|
||||
// test.todo('should paginate within the dropdown');
|
||||
// test.todo('should search within the relationship field');
|
||||
|
||||
test('should show useAsTitle on relation', async () => {
|
||||
await page.goto(url.edit(docWithExistingRelations.id));
|
||||
|
||||
const fields = page.locator('.render-fields >> .react-select');
|
||||
const relationWithTitleField = fields.nth(4);
|
||||
const field = page.locator('#field-relationshipWithTitle .react-select');
|
||||
|
||||
// Check existing relationship for correct title
|
||||
await expect(relationWithTitleField).toHaveText(relationWithTitle.name);
|
||||
await expect(field).toHaveText(relationWithTitle.name);
|
||||
|
||||
await relationWithTitleField.click({ delay: 100 });
|
||||
await field.click({ delay: 100 });
|
||||
const options = page.locator('.rs__option');
|
||||
|
||||
await expect(options).toHaveCount(2); // None + 1 Doc
|
||||
});
|
||||
|
||||
test('should show id on relation in list view', async () => {
|
||||
await page.goto(url.list);
|
||||
await wait(110);
|
||||
const cells = page.locator('.cell-relationship');
|
||||
const relationship = cells.nth(0);
|
||||
await expect(relationship).toHaveText(relationOneDoc.id);
|
||||
});
|
||||
|
||||
test('should show Untitled ID on restricted relation in list view', async () => {
|
||||
await page.goto(url.list);
|
||||
await wait(110);
|
||||
const cells = page.locator('.cell-relationshipRestricted');
|
||||
const relationship = cells.nth(0);
|
||||
await expect(relationship).toContainText('Untitled - ID: ');
|
||||
});
|
||||
|
||||
test('should show useAsTitle on relation in list view', async () => {
|
||||
await page.goto(url.list);
|
||||
await wait(110);
|
||||
const cells = page.locator('.cell-relationshipWithTitle');
|
||||
const relationship = cells.nth(0);
|
||||
await expect(relationship).toHaveText(relationWithTitle.name);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
94
test/e2e/fields-relationship/payload-types.ts
Normal file
94
test/e2e/fields-relationship/payload-types.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload CMS.
|
||||
* 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 {}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "fields-relationship".
|
||||
*/
|
||||
export interface FieldsRelationship {
|
||||
id: string;
|
||||
relationship?: string | RelationOne;
|
||||
relationshipHasMany?: (string | RelationOne)[];
|
||||
relationshipMultiple?:
|
||||
| {
|
||||
value: string | RelationOne;
|
||||
relationTo: 'relation-one';
|
||||
}
|
||||
| {
|
||||
value: string | RelationTwo;
|
||||
relationTo: 'relation-two';
|
||||
};
|
||||
relationshipHasManyMultiple?: (
|
||||
| {
|
||||
value: string | RelationOne;
|
||||
relationTo: 'relation-one';
|
||||
}
|
||||
| {
|
||||
value: string | RelationTwo;
|
||||
relationTo: 'relation-two';
|
||||
}
|
||||
)[];
|
||||
relationshipRestricted?: string | RelationRestricted;
|
||||
relationshipWithTitle?: string | RelationWithTitle;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "relation-one".
|
||||
*/
|
||||
export interface RelationOne {
|
||||
id: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "relation-two".
|
||||
*/
|
||||
export interface RelationTwo {
|
||||
id: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "relation-restricted".
|
||||
*/
|
||||
export interface RelationRestricted {
|
||||
id: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "relation-with-title".
|
||||
*/
|
||||
export interface RelationWithTitle {
|
||||
id: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
32
test/e2e/fields/payload-types.ts
Normal file
32
test/e2e/fields/payload-types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload CMS.
|
||||
* 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 {}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "text-fields".
|
||||
*/
|
||||
export interface TextField {
|
||||
id: string;
|
||||
text: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -17,9 +17,9 @@ export async function firstRegister(args: FirstRegisterArgs): Promise<void> {
|
||||
const { page, serverURL } = args;
|
||||
|
||||
await page.goto(`${serverURL}/admin`);
|
||||
await page.fill('#email', devUser.email);
|
||||
await page.fill('#password', devUser.password);
|
||||
await page.fill('#confirm-password', devUser.password);
|
||||
await page.fill('#field-email', devUser.email);
|
||||
await page.fill('#field-password', devUser.password);
|
||||
await page.fill('#field-confirm-password', devUser.password);
|
||||
await wait(500);
|
||||
await page.click('[type=submit]');
|
||||
await page.waitForURL(`${serverURL}/admin`);
|
||||
@@ -29,15 +29,15 @@ export async function login(args: LoginArgs): Promise<void> {
|
||||
const { page, serverURL } = args;
|
||||
|
||||
await page.goto(`${serverURL}/admin`);
|
||||
await page.fill('#email', devUser.email);
|
||||
await page.fill('#password', devUser.password);
|
||||
await page.fill('#field-email', devUser.email);
|
||||
await page.fill('#field-password', devUser.password);
|
||||
await wait(500);
|
||||
await page.click('[type=submit]');
|
||||
await page.waitForURL(`${serverURL}/admin`);
|
||||
}
|
||||
|
||||
export async function saveDocAndAssert(page: Page): Promise<void> {
|
||||
await page.click('text=Save', { delay: 100 });
|
||||
await page.click('#action-save', { delay: 100 });
|
||||
await expect(page.locator('.Toastify')).toContainText('successfully');
|
||||
expect(page.url()).not.toContain('create');
|
||||
}
|
||||
|
||||
33
test/e2e/localization/payload-types.ts
Normal file
33
test/e2e/localization/payload-types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload CMS.
|
||||
* 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 {}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "localized-posts".
|
||||
*/
|
||||
export interface LocalizedPost {
|
||||
id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
1
test/e2e/uploads/.gitignore
vendored
Normal file
1
test/e2e/uploads/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/media
|
||||
102
test/e2e/uploads/config.ts
Normal file
102
test/e2e/uploads/config.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { buildConfig } from '../buildConfig';
|
||||
import { devUser } from '../../credentials';
|
||||
import getFileByPath from '../../../src/uploads/getFileByPath';
|
||||
|
||||
export const mediaSlug = 'media';
|
||||
|
||||
export const relationSlug = 'relation';
|
||||
|
||||
const mockModulePath = path.resolve(__dirname, './mocks/mockFSModule.js');
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
webpack: (config) => ({
|
||||
...config,
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
alias: {
|
||||
...config.resolve.alias,
|
||||
fs: mockModulePath,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
// upload: {},
|
||||
collections: [
|
||||
{
|
||||
slug: relationSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'image',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: mediaSlug,
|
||||
upload: {
|
||||
staticURL: '/media',
|
||||
staticDir: './media',
|
||||
imageSizes: [
|
||||
{
|
||||
name: 'maintainedAspectRatio',
|
||||
width: 1024,
|
||||
height: null,
|
||||
crop: 'center',
|
||||
},
|
||||
{
|
||||
name: 'tablet',
|
||||
width: 640,
|
||||
height: 480,
|
||||
crop: 'left top',
|
||||
},
|
||||
{
|
||||
name: 'mobile',
|
||||
width: 320,
|
||||
height: 240,
|
||||
crop: 'left top',
|
||||
},
|
||||
{
|
||||
name: 'icon',
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: [
|
||||
],
|
||||
},
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
// delete files in /media
|
||||
const mediaDir = path.resolve(__dirname, './media');
|
||||
fs.readdirSync(mediaDir).forEach((f) => fs.rmSync(`${mediaDir}/${f}`));
|
||||
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
});
|
||||
// Create image
|
||||
const filePath = path.resolve(__dirname, './image.png');
|
||||
const file = getFileByPath(filePath);
|
||||
|
||||
const { id: uploadedImage } = await payload.create({
|
||||
collection: mediaSlug,
|
||||
data: {},
|
||||
file,
|
||||
});
|
||||
|
||||
await payload.create({
|
||||
collection: relationSlug,
|
||||
data: {
|
||||
image: uploadedImage,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
BIN
test/e2e/uploads/image.png
Normal file
BIN
test/e2e/uploads/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
88
test/e2e/uploads/index.spec.ts
Normal file
88
test/e2e/uploads/index.spec.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { AdminUrlUtil } from '../../helpers/adminUrlUtil';
|
||||
import { initPayloadE2E } from '../../helpers/configHelpers';
|
||||
import { login, saveDocAndAssert } from '../helpers';
|
||||
import { relationSlug, mediaSlug } from './config';
|
||||
import type { Media } from './payload-types';
|
||||
import wait from '../../../src/utilities/wait';
|
||||
import payload from '../../../src';
|
||||
|
||||
const { beforeAll, describe } = test;
|
||||
|
||||
let mediaURL: AdminUrlUtil;
|
||||
let relationURL: AdminUrlUtil;
|
||||
|
||||
describe('uploads', () => {
|
||||
let page: Page;
|
||||
let mediaDoc: Media;
|
||||
|
||||
beforeAll(async ({ browser }) => {
|
||||
const { serverURL } = await initPayloadE2E(__dirname);
|
||||
|
||||
mediaURL = new AdminUrlUtil(serverURL, mediaSlug);
|
||||
relationURL = new AdminUrlUtil(serverURL, relationSlug);
|
||||
|
||||
const context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
|
||||
const findMedia = await payload.find({
|
||||
collection: mediaSlug,
|
||||
depth: 0,
|
||||
pagination: false,
|
||||
});
|
||||
mediaDoc = findMedia.docs[0] as Media;
|
||||
|
||||
await login({ page, serverURL });
|
||||
});
|
||||
|
||||
test('should see upload filename in relation list', async () => {
|
||||
await page.goto(relationURL.list);
|
||||
|
||||
await wait(110);
|
||||
const field = page.locator('.cell-image');
|
||||
|
||||
await expect(field).toContainText('image.png');
|
||||
});
|
||||
|
||||
test('should show upload filename in upload collection list', async () => {
|
||||
await page.goto(mediaURL.list);
|
||||
|
||||
const media = page.locator('.upload-card__filename');
|
||||
await wait(110);
|
||||
|
||||
await expect(media).toHaveText('image.png');
|
||||
});
|
||||
|
||||
test('should create file upload', async () => {
|
||||
await page.goto(mediaURL.create);
|
||||
|
||||
await page.setInputFiles('input[type="file"]', './image.png');
|
||||
|
||||
const filename = page.locator('.file-field__filename');
|
||||
|
||||
await expect(filename).toContainText('.png');
|
||||
|
||||
await page.locator('.form-submit button').click();
|
||||
|
||||
await saveDocAndAssert(page);
|
||||
});
|
||||
|
||||
test('should show resized images', async () => {
|
||||
await page.goto(mediaURL.edit(mediaDoc.id));
|
||||
|
||||
await page.locator('.btn.file-details__toggle-more-info').click();
|
||||
|
||||
const maintainedAspectRatioMeta = page.locator('.file-details__sizes .file-meta').nth(0);
|
||||
await expect(maintainedAspectRatioMeta).toContainText('1024x1024');
|
||||
|
||||
const tabletMeta = page.locator('.file-details__sizes .file-meta').nth(1);
|
||||
await expect(tabletMeta).toContainText('640x480');
|
||||
|
||||
const mobileMeta = page.locator('.file-details__sizes .file-meta').nth(2);
|
||||
await expect(mobileMeta).toContainText('320x240');
|
||||
|
||||
const iconMeta = page.locator('.file-details__sizes .file-meta').nth(3);
|
||||
await expect(iconMeta).toContainText('16x16');
|
||||
});
|
||||
});
|
||||
4
test/e2e/uploads/mocks/mockFSModule.js
Normal file
4
test/e2e/uploads/mocks/mockFSModule.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
readdirSync: () => {},
|
||||
rmSync: () => {},
|
||||
};
|
||||
81
test/e2e/uploads/payload-types.ts
Normal file
81
test/e2e/uploads/payload-types.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload CMS.
|
||||
* 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 {}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "relation".
|
||||
*/
|
||||
export interface Relation {
|
||||
id: string;
|
||||
image?: string | Media;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media".
|
||||
*/
|
||||
export interface Media {
|
||||
id: string;
|
||||
url?: string;
|
||||
filename?: string;
|
||||
mimeType?: string;
|
||||
filesize?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
sizes?: {
|
||||
maintainedAspectRatio?: {
|
||||
url?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
mimeType?: string;
|
||||
filesize?: number;
|
||||
filename?: string;
|
||||
};
|
||||
tablet?: {
|
||||
url?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
mimeType?: string;
|
||||
filesize?: number;
|
||||
filename?: string;
|
||||
};
|
||||
mobile?: {
|
||||
url?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
mimeType?: string;
|
||||
filesize?: number;
|
||||
filename?: string;
|
||||
};
|
||||
icon?: {
|
||||
url?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
mimeType?: string;
|
||||
filesize?: number;
|
||||
filename?: string;
|
||||
};
|
||||
};
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
31
test/e2e/versions/payload-types.ts
Normal file
31
test/e2e/versions/payload-types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload CMS.
|
||||
* 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 {}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "slugname".
|
||||
*/
|
||||
export interface Slugname {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import qs from 'qs';
|
||||
import type { Config } from '../../src/config/types';
|
||||
import type { PaginatedDocs } from '../../src/mongoose/types';
|
||||
import type { Where } from '../../src/types';
|
||||
import { devUser } from '../credentials';
|
||||
|
||||
require('isomorphic-fetch');
|
||||
|
||||
@@ -68,8 +69,8 @@ export class RESTClient {
|
||||
|
||||
async login(incomingArgs?: LoginArgs): Promise<string> {
|
||||
const args = incomingArgs ?? {
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'test',
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
collection: 'users',
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import mongoose from 'mongoose';
|
||||
import { initPayloadTest } from '../../helpers/configHelpers';
|
||||
import payload from '../../../src';
|
||||
import config from './config';
|
||||
import type { Array as ArrayCollection } from './payload-types';
|
||||
|
||||
const collection = config.collections[0]?.slug;
|
||||
|
||||
@@ -47,7 +48,7 @@ describe('array-update', () => {
|
||||
};
|
||||
|
||||
|
||||
const updatedDoc = await payload.update({
|
||||
const updatedDoc = await payload.update<ArrayCollection>({
|
||||
id: doc.id,
|
||||
collection,
|
||||
data: {
|
||||
@@ -55,7 +56,7 @@ describe('array-update', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(updatedDoc.array[0]).toMatchObject({
|
||||
expect(updatedDoc.array?.[0]).toMatchObject({
|
||||
required: updatedText,
|
||||
optional: originalText,
|
||||
});
|
||||
@@ -69,7 +70,7 @@ describe('array-update', () => {
|
||||
optional: 'optional test',
|
||||
};
|
||||
|
||||
const doc = await payload.create({
|
||||
const doc = await payload.create<ArrayCollection>({
|
||||
collection,
|
||||
data: {
|
||||
array: [
|
||||
@@ -82,7 +83,7 @@ describe('array-update', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const updatedDoc = await payload.update<any>({
|
||||
const updatedDoc = await payload.update<ArrayCollection>({
|
||||
id: doc.id,
|
||||
collection,
|
||||
data: {
|
||||
@@ -91,8 +92,8 @@ describe('array-update', () => {
|
||||
required: updatedText,
|
||||
},
|
||||
{
|
||||
id: doc.array[1].id,
|
||||
required: doc.array[1].required,
|
||||
id: doc.array?.[1].id,
|
||||
required: doc.array?.[1].required as string,
|
||||
// NOTE - not passing optional field. It should persist
|
||||
// because we're passing ID
|
||||
},
|
||||
@@ -100,9 +101,9 @@ describe('array-update', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(updatedDoc.array[0].required).toStrictEqual(updatedText);
|
||||
expect(updatedDoc.array[0].optional).toBeUndefined();
|
||||
expect(updatedDoc.array?.[0].required).toStrictEqual(updatedText);
|
||||
expect(updatedDoc.array?.[0].optional).toBeUndefined();
|
||||
|
||||
expect(updatedDoc.array[1]).toMatchObject(secondArrayItem);
|
||||
expect(updatedDoc.array?.[1]).toMatchObject(secondArrayItem);
|
||||
});
|
||||
});
|
||||
|
||||
36
test/int/array-update/payload-types.ts
Normal file
36
test/int/array-update/payload-types.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload CMS.
|
||||
* 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 {}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "arrays".
|
||||
*/
|
||||
export interface Array {
|
||||
id: string;
|
||||
array?: {
|
||||
required: string;
|
||||
optional?: string;
|
||||
id?: string;
|
||||
}[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -3,7 +3,11 @@ import { buildConfig as buildPayloadConfig } from '../../src/config/build';
|
||||
import type { Config, SanitizedConfig } from '../../src/config/types';
|
||||
|
||||
export function buildConfig(overrides?: Partial<Config>): SanitizedConfig {
|
||||
const baseConfig: Config = {};
|
||||
const baseConfig: Config = {
|
||||
typescript: {
|
||||
outputFile: process.env.PAYLOAD_TS_OUTPUT_PATH,
|
||||
},
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
baseConfig.admin = {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { initPayloadTest } from '../../helpers/configHelpers';
|
||||
import config from './config';
|
||||
import payload from '../../../src';
|
||||
import { RESTClient } from '../../helpers/rest';
|
||||
import type { Post } from './payload-types';
|
||||
|
||||
const collection = config.collections[0]?.slug;
|
||||
|
||||
@@ -11,7 +12,7 @@ let client: RESTClient;
|
||||
describe('collections-graphql', () => {
|
||||
beforeAll(async () => {
|
||||
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } });
|
||||
client = new RESTClient(config, { serverURL });
|
||||
client = new RESTClient(config, { serverURL, defaultSlug: collection });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -23,7 +24,7 @@ describe('collections-graphql', () => {
|
||||
it('should create', async () => {
|
||||
const title = 'hello';
|
||||
|
||||
const { doc } = await client.create({
|
||||
const { doc } = await client.create<Post>({
|
||||
slug: collection,
|
||||
data: {
|
||||
title,
|
||||
|
||||
32
test/int/collections-graphql/payload-types.ts
Normal file
32
test/int/collections-graphql/payload-types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload CMS.
|
||||
* 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 {}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: string;
|
||||
title?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -1,23 +1,13 @@
|
||||
import type { CollectionConfig } from '../../../src/collections/config/types';
|
||||
import { devUser } from '../../credentials';
|
||||
import { buildConfig } from '../buildConfig';
|
||||
|
||||
export interface Post {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
number?: number;
|
||||
relationField?: Relation | string
|
||||
relationHasManyField?: RelationHasMany[] | string[]
|
||||
relationMultiRelationTo?: Relation[] | string[]
|
||||
}
|
||||
import type { Post } from './payload-types';
|
||||
|
||||
export interface Relation {
|
||||
id: string
|
||||
name: string
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type RelationHasMany = Relation
|
||||
|
||||
const openAccess = {
|
||||
create: () => true,
|
||||
read: () => true,
|
||||
@@ -25,9 +15,9 @@ const openAccess = {
|
||||
delete: () => true,
|
||||
};
|
||||
|
||||
const collectionWithName = (slug: string): CollectionConfig => {
|
||||
const collectionWithName = (collectionSlug: string): CollectionConfig => {
|
||||
return {
|
||||
slug,
|
||||
slug: collectionSlug,
|
||||
access: openAccess,
|
||||
fields: [
|
||||
{
|
||||
@@ -39,9 +29,7 @@ const collectionWithName = (slug: string): CollectionConfig => {
|
||||
};
|
||||
|
||||
export const slug = 'posts';
|
||||
export const relationSlug = 'relation-normal';
|
||||
export const relationHasManySlug = 'relation-has-many';
|
||||
export const relationMultipleRelationToSlug = 'relation-multi-relation-to';
|
||||
export const relationSlug = 'relation';
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
{
|
||||
@@ -70,7 +58,7 @@ export default buildConfig({
|
||||
{
|
||||
name: 'relationHasManyField',
|
||||
type: 'relationship',
|
||||
relationTo: relationHasManySlug,
|
||||
relationTo: relationSlug,
|
||||
hasMany: true,
|
||||
},
|
||||
// Relation multiple relationTo
|
||||
@@ -79,27 +67,84 @@ export default buildConfig({
|
||||
type: 'relationship',
|
||||
relationTo: [relationSlug, 'dummy'],
|
||||
},
|
||||
// Relation multiple relationTo hasMany
|
||||
{
|
||||
name: 'relationMultiRelationToHasMany',
|
||||
type: 'relationship',
|
||||
relationTo: [relationSlug, 'dummy'],
|
||||
hasMany: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
collectionWithName(relationSlug),
|
||||
collectionWithName(relationHasManySlug),
|
||||
collectionWithName('dummy'),
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
const rel1 = await payload.create<RelationHasMany>({
|
||||
collection: relationHasManySlug,
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
});
|
||||
|
||||
const rel1 = await payload.create<Relation>({
|
||||
collection: relationSlug,
|
||||
data: {
|
||||
name: 'name',
|
||||
},
|
||||
});
|
||||
const rel2 = await payload.create<Relation>({
|
||||
collection: relationSlug,
|
||||
data: {
|
||||
name: 'name2',
|
||||
},
|
||||
});
|
||||
|
||||
await payload.create({
|
||||
// Relation - hasMany
|
||||
await payload.create<Post>({
|
||||
collection: slug,
|
||||
data: {
|
||||
title: 'title',
|
||||
title: 'rel to hasMany',
|
||||
relationHasManyField: rel1.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
await payload.create<Post>({
|
||||
collection: slug,
|
||||
data: {
|
||||
title: 'rel to hasMany 2',
|
||||
relationHasManyField: rel2.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Relation - relationTo multi
|
||||
await payload.create<Post>({
|
||||
collection: slug,
|
||||
data: {
|
||||
title: 'rel to multi',
|
||||
relationMultiRelationTo: {
|
||||
relationTo: relationSlug,
|
||||
value: rel2.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Relation - relationTo multi hasMany
|
||||
await payload.create<Post>({
|
||||
collection: slug,
|
||||
data: {
|
||||
title: 'rel to multi hasMany',
|
||||
relationMultiRelationToHasMany: [
|
||||
{
|
||||
relationTo: relationSlug,
|
||||
value: rel1.id,
|
||||
},
|
||||
{
|
||||
relationTo: relationSlug,
|
||||
value: rel2.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { initPayloadTest } from '../../helpers/configHelpers';
|
||||
import type { Relation, Post, RelationHasMany } from './config';
|
||||
import config, { relationHasManySlug, slug, relationSlug } from './config';
|
||||
import type { Relation } from './config';
|
||||
import config, { slug, relationSlug } from './config';
|
||||
import payload from '../../../src';
|
||||
import { RESTClient } from '../../helpers/rest';
|
||||
import { mapAsync } from '../../../src/utilities/mapAsync';
|
||||
import type { Post } from './payload-types';
|
||||
|
||||
let client: RESTClient;
|
||||
|
||||
@@ -85,54 +86,75 @@ describe('collections-rest', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Querying', () => {
|
||||
describe('Relationships', () => {
|
||||
it('should query nested relationship', async () => {
|
||||
const nameToQuery = 'name';
|
||||
const { doc: relation } = await client.create<Relation>({
|
||||
let post: Post;
|
||||
let relation: Relation;
|
||||
let relation2: Relation;
|
||||
const nameToQuery = 'name';
|
||||
const nameToQuery2 = 'name';
|
||||
|
||||
beforeEach(async () => {
|
||||
({ doc: relation } = await client.create<Relation>({
|
||||
slug: relationSlug,
|
||||
data: {
|
||||
name: nameToQuery,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
const post1 = await createPost({
|
||||
({ doc: relation2 } = await client.create<Relation>({
|
||||
slug: relationSlug,
|
||||
data: {
|
||||
name: nameToQuery2,
|
||||
},
|
||||
}));
|
||||
|
||||
post = await createPost({
|
||||
relationField: relation.id,
|
||||
});
|
||||
await createPost();
|
||||
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
'relationField.name': {
|
||||
equals: nameToQuery,
|
||||
await createPost(); // Extra post to allow asserting totalDoc count
|
||||
});
|
||||
|
||||
describe('regular relationship', () => {
|
||||
it('query by property value', async () => {
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
'relationField.name': {
|
||||
equals: relation.name,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.docs).toEqual([post]);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.docs).toEqual([post1]);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
it('query by id', async () => {
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
relationField: {
|
||||
equals: relation.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.docs).toEqual([post]);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should query nested relationship - hasMany', async () => {
|
||||
const nameToQuery = 'name';
|
||||
const { doc: relation } = await client.create<RelationHasMany>({
|
||||
slug: relationHasManySlug,
|
||||
data: {
|
||||
name: nameToQuery,
|
||||
},
|
||||
});
|
||||
|
||||
const post1 = await createPost({
|
||||
relationHasManyField: [relation.id],
|
||||
relationHasManyField: [relation.id, relation2.id],
|
||||
});
|
||||
await createPost();
|
||||
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
'relationHasManyField.name': {
|
||||
equals: nameToQuery,
|
||||
equals: relation.name,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -140,6 +162,81 @@ describe('collections-rest', () => {
|
||||
expect(status).toEqual(200);
|
||||
expect(result.docs).toEqual([post1]);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
|
||||
// Query second relationship
|
||||
const { status: status2, result: result2 } = await client.find<Post>({
|
||||
query: {
|
||||
'relationHasManyField.name': {
|
||||
equals: relation2.name,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status2).toEqual(200);
|
||||
expect(result2.docs).toEqual([post1]);
|
||||
expect(result2.totalDocs).toEqual(1);
|
||||
});
|
||||
|
||||
describe('relationTo multi', () => {
|
||||
it('nested by id', async () => {
|
||||
const post1 = await createPost({
|
||||
relationMultiRelationTo: { relationTo: relationSlug, value: relation.id },
|
||||
});
|
||||
await createPost();
|
||||
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
'relationMultiRelationTo.value': {
|
||||
equals: relation.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.docs).toEqual([post1]);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
});
|
||||
|
||||
it.todo('nested by property value');
|
||||
});
|
||||
|
||||
describe('relationTo multi hasMany', () => {
|
||||
it('nested by id', async () => {
|
||||
const post1 = await createPost({
|
||||
relationMultiRelationToHasMany: [
|
||||
{ relationTo: relationSlug, value: relation.id },
|
||||
{ relationTo: relationSlug, value: relation2.id },
|
||||
],
|
||||
});
|
||||
await createPost();
|
||||
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
'relationMultiRelationToHasMany.value': {
|
||||
equals: relation.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(result.docs).toEqual([post1]);
|
||||
expect(result.totalDocs).toEqual(1);
|
||||
|
||||
// Query second relation
|
||||
const { status: status2, result: result2 } = await client.find<Post>({
|
||||
query: {
|
||||
'relationMultiRelationToHasMany.value': {
|
||||
equals: relation.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status2).toEqual(200);
|
||||
expect(result2.docs).toEqual([post1]);
|
||||
expect(result2.totalDocs).toEqual(1);
|
||||
});
|
||||
|
||||
it.todo('nested by property value');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -215,7 +312,7 @@ describe('collections-rest', () => {
|
||||
const { status, result } = await client.find<Post>({
|
||||
query: {
|
||||
title: {
|
||||
like: post1.title.substring(0, 6),
|
||||
like: post1.title?.substring(0, 6),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -412,7 +509,6 @@ describe('collections-rest', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
async function createPost(overrides?: Partial<Post>) {
|
||||
const { doc } = await client.create<Post>({ data: { title: 'title', ...overrides } });
|
||||
return doc;
|
||||
|
||||
75
test/int/collections-rest/payload-types.ts
Normal file
75
test/int/collections-rest/payload-types.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload CMS.
|
||||
* 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 {}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
number?: number;
|
||||
relationField?: string | Relation;
|
||||
relationHasManyField?: (string | Relation)[];
|
||||
relationMultiRelationTo?:
|
||||
| {
|
||||
value: string | Relation;
|
||||
relationTo: 'relation';
|
||||
}
|
||||
| {
|
||||
value: string | Dummy;
|
||||
relationTo: 'dummy';
|
||||
};
|
||||
relationMultiRelationToHasMany?: (
|
||||
| {
|
||||
value: string | Relation;
|
||||
relationTo: 'relation';
|
||||
}
|
||||
| {
|
||||
value: string | Dummy;
|
||||
relationTo: 'dummy';
|
||||
}
|
||||
)[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "relation".
|
||||
*/
|
||||
export interface Relation {
|
||||
id: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "dummy".
|
||||
*/
|
||||
export interface Dummy {
|
||||
id: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
Reference in New Issue
Block a user