test: port relationships spec
This commit is contained in:
214
test/relationships/config.ts
Normal file
214
test/relationships/config.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import type { CollectionConfig } from '../../src/collections/config/types';
|
||||
import { devUser } from '../credentials';
|
||||
import { buildConfig } from '../buildConfig';
|
||||
import type { CustomIdRelation, Post, Relation } from './payload-types';
|
||||
|
||||
const openAccess = {
|
||||
create: () => true,
|
||||
read: () => true,
|
||||
update: () => true,
|
||||
delete: () => true,
|
||||
};
|
||||
|
||||
const defaultAccess = ({ req: { user } }) => Boolean(user);
|
||||
|
||||
const collectionWithName = (collectionSlug: string): CollectionConfig => {
|
||||
return {
|
||||
slug: collectionSlug,
|
||||
access: openAccess,
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'disableRelation', // used filteredRelation
|
||||
type: 'checkbox',
|
||||
required: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const slug = 'posts';
|
||||
export const relationSlug = 'relation';
|
||||
export const defaultAccessRelSlug = 'strict-access';
|
||||
export const chainedRelSlug = 'chained-relation';
|
||||
export const customIdSlug = 'custom-id-relation';
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
{
|
||||
slug,
|
||||
access: openAccess,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
type: 'number',
|
||||
},
|
||||
// Relationship
|
||||
{
|
||||
name: 'relationField',
|
||||
type: 'relationship',
|
||||
relationTo: relationSlug,
|
||||
},
|
||||
// Relationship w/ default access
|
||||
{
|
||||
name: 'defaultAccessRelation',
|
||||
type: 'relationship',
|
||||
relationTo: defaultAccessRelSlug,
|
||||
},
|
||||
{
|
||||
name: 'chainedRelation',
|
||||
type: 'relationship',
|
||||
relationTo: chainedRelSlug,
|
||||
},
|
||||
{
|
||||
name: 'maxDepthRelation',
|
||||
maxDepth: 0,
|
||||
type: 'relationship',
|
||||
relationTo: relationSlug,
|
||||
},
|
||||
{
|
||||
name: 'customIdRelation',
|
||||
type: 'relationship',
|
||||
relationTo: customIdSlug,
|
||||
},
|
||||
{
|
||||
name: 'filteredRelation',
|
||||
type: 'relationship',
|
||||
relationTo: relationSlug,
|
||||
filterOptions: {
|
||||
disableRelation: {
|
||||
not_equals: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
collectionWithName(relationSlug),
|
||||
{
|
||||
...collectionWithName(defaultAccessRelSlug),
|
||||
access: {
|
||||
create: defaultAccess,
|
||||
read: defaultAccess,
|
||||
update: defaultAccess,
|
||||
delete: defaultAccess,
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: chainedRelSlug,
|
||||
access: openAccess,
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'relation',
|
||||
type: 'relationship',
|
||||
relationTo: chainedRelSlug,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: customIdSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
});
|
||||
|
||||
const rel1 = await payload.create<Relation>({
|
||||
collection: relationSlug,
|
||||
data: {
|
||||
name: 'name',
|
||||
},
|
||||
});
|
||||
|
||||
const filteredRelation = await payload.create<Relation>({
|
||||
collection: relationSlug,
|
||||
data: {
|
||||
name: 'filtered',
|
||||
},
|
||||
});
|
||||
|
||||
const defaultAccessRelation = await payload.create<Relation>({
|
||||
collection: defaultAccessRelSlug,
|
||||
data: {
|
||||
name: 'name',
|
||||
},
|
||||
});
|
||||
|
||||
const chained3 = await payload.create<Relation>({
|
||||
collection: chainedRelSlug,
|
||||
data: {
|
||||
name: 'chain3',
|
||||
},
|
||||
});
|
||||
|
||||
const chained2 = await payload.create<Relation>({
|
||||
collection: chainedRelSlug,
|
||||
data: {
|
||||
name: 'chain2',
|
||||
relation: chained3.id,
|
||||
},
|
||||
});
|
||||
|
||||
const chained = await payload.create<Relation>({
|
||||
collection: chainedRelSlug,
|
||||
data: {
|
||||
name: 'chain1',
|
||||
relation: chained2.id,
|
||||
},
|
||||
});
|
||||
|
||||
const customIdRelation = await payload.create<CustomIdRelation>({
|
||||
collection: customIdSlug,
|
||||
data: {
|
||||
id: 'custommmm',
|
||||
name: 'custom-id',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// Relationship
|
||||
await payload.create<Post>({
|
||||
collection: slug,
|
||||
data: {
|
||||
title: 'with relationship',
|
||||
relationField: rel1.id,
|
||||
defaultAccessRelation: defaultAccessRelation.id,
|
||||
chainedRelation: chained.id,
|
||||
maxDepthRelation: rel1.id,
|
||||
customIdRelation: customIdRelation.id,
|
||||
filteredRelation: filteredRelation.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
182
test/relationships/int.spec.ts
Normal file
182
test/relationships/int.spec.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { initPayloadTest } from '../helpers/configHelpers';
|
||||
import config, { customIdSlug, chainedRelSlug, defaultAccessRelSlug, slug, relationSlug } from './config';
|
||||
import payload from '../../src';
|
||||
import { RESTClient } from '../helpers/rest';
|
||||
import type { ChainedRelation, CustomIdRelation, Post, Relation } from './payload-types';
|
||||
import { mapAsync } from '../../src/utilities/mapAsync';
|
||||
|
||||
let client: RESTClient;
|
||||
|
||||
type EasierChained = { relation: EasierChained, id: string }
|
||||
|
||||
describe('Relationships', () => {
|
||||
beforeAll(async () => {
|
||||
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } });
|
||||
client = new RESTClient(config, { serverURL, defaultSlug: slug });
|
||||
await client.login();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.connection.dropDatabase();
|
||||
await mongoose.connection.close();
|
||||
await payload.mongoMemoryServer.stop();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await clearDocs();
|
||||
});
|
||||
|
||||
describe('Querying', () => {
|
||||
describe('Relationships', () => {
|
||||
let post: Post;
|
||||
let relation: Relation;
|
||||
let filteredRelation: Relation;
|
||||
let defaultAccessRelation: Relation;
|
||||
let chained: ChainedRelation;
|
||||
let chained2: ChainedRelation;
|
||||
let chained3: ChainedRelation;
|
||||
let customIdRelation: CustomIdRelation;
|
||||
let generatedCustomId: string;
|
||||
const nameToQuery = 'name';
|
||||
|
||||
beforeEach(async () => {
|
||||
relation = await payload.create<Relation>({
|
||||
collection: relationSlug,
|
||||
data: {
|
||||
name: nameToQuery,
|
||||
},
|
||||
});
|
||||
|
||||
filteredRelation = await payload.create<Relation>({
|
||||
collection: relationSlug,
|
||||
data: {
|
||||
name: nameToQuery,
|
||||
disableRelation: false,
|
||||
},
|
||||
});
|
||||
|
||||
defaultAccessRelation = await payload.create<Relation>({
|
||||
collection: defaultAccessRelSlug,
|
||||
data: {
|
||||
name: 'default access',
|
||||
},
|
||||
});
|
||||
|
||||
chained3 = await payload.create<ChainedRelation>({
|
||||
collection: chainedRelSlug,
|
||||
data: {
|
||||
name: 'chain3',
|
||||
},
|
||||
});
|
||||
|
||||
chained2 = await payload.create<ChainedRelation>({
|
||||
collection: chainedRelSlug,
|
||||
data: {
|
||||
name: 'chain2',
|
||||
relation: chained3.id,
|
||||
},
|
||||
});
|
||||
|
||||
chained = await payload.create<ChainedRelation>({
|
||||
collection: chainedRelSlug,
|
||||
data: {
|
||||
name: 'chain1',
|
||||
relation: chained2.id,
|
||||
},
|
||||
});
|
||||
|
||||
generatedCustomId = `custom-${randomBytes(32).toString('hex').slice(0, 12)}`;
|
||||
customIdRelation = await payload.create<CustomIdRelation>({
|
||||
collection: customIdSlug,
|
||||
data: {
|
||||
id: generatedCustomId,
|
||||
name: 'custom-id',
|
||||
},
|
||||
});
|
||||
|
||||
post = await createPost({
|
||||
relationField: relation.id,
|
||||
defaultAccessRelation: defaultAccessRelation.id,
|
||||
chainedRelation: chained.id,
|
||||
maxDepthRelation: relation.id,
|
||||
customIdRelation: customIdRelation.id,
|
||||
filteredRelation: filteredRelation.id,
|
||||
});
|
||||
|
||||
await createPost(); // Extra post to allow asserting totalDoc count
|
||||
});
|
||||
|
||||
it('should prevent an unauthorized population of strict access', async () => {
|
||||
const { doc } = await client.findByID<Post>({ id: post.id, auth: false });
|
||||
expect(doc.defaultAccessRelation).toEqual(defaultAccessRelation.id);
|
||||
});
|
||||
|
||||
it('should populate strict access when authorized', async () => {
|
||||
const { doc } = await client.findByID<Post>({ id: post.id });
|
||||
expect(doc.defaultAccessRelation).toEqual(defaultAccessRelation);
|
||||
});
|
||||
|
||||
it('should use filterOptions to limit relationship options', async () => {
|
||||
const { doc } = await client.findByID<Post>({ id: post.id });
|
||||
|
||||
expect(doc.filteredRelation).toMatchObject({ id: filteredRelation.id });
|
||||
|
||||
await client.update<Relation>({ id: filteredRelation.id, slug: relationSlug, data: { disableRelation: true } });
|
||||
|
||||
const { doc: docAfterUpdatingRel } = await client.findByID<Post>({ id: post.id });
|
||||
|
||||
// No change to existing relation
|
||||
expect(docAfterUpdatingRel.filteredRelation).toMatchObject({ id: filteredRelation.id });
|
||||
|
||||
// Attempt to update post with a now filtered relation
|
||||
const { status, errors } = await client.update<Post>({ id: post.id, data: { filteredRelation: filteredRelation.id } });
|
||||
|
||||
expect(errors?.[0]).toMatchObject({ name: 'ValidationError', message: expect.any(String), data: expect.anything() });
|
||||
expect(status).toEqual(400);
|
||||
});
|
||||
|
||||
describe('depth', () => {
|
||||
it('should populate to depth', async () => {
|
||||
const { doc } = await client.findByID<Post>({ id: post.id, options: { depth: 2 } });
|
||||
const depth0 = doc?.chainedRelation as EasierChained;
|
||||
expect(depth0.id).toEqual(chained.id);
|
||||
expect(depth0.relation.id).toEqual(chained2.id);
|
||||
expect(depth0.relation.relation as unknown as string).toEqual(chained3.id);
|
||||
expect(depth0.relation.relation).toEqual(chained3.id);
|
||||
});
|
||||
|
||||
it('should only populate ID if depth 0', async () => {
|
||||
const { doc } = await client.findByID<Post>({ id: post.id, options: { depth: 0 } });
|
||||
expect(doc?.chainedRelation).toEqual(chained.id);
|
||||
});
|
||||
|
||||
it('should respect maxDepth at field level', async () => {
|
||||
const { doc } = await client.findByID<Post>({ id: post.id, options: { depth: 1 } });
|
||||
expect(doc?.maxDepthRelation).toEqual(relation.id);
|
||||
expect(doc?.maxDepthRelation).not.toHaveProperty('name');
|
||||
// should not affect other fields
|
||||
expect(doc?.relationField).toMatchObject({ id: relation.id, name: relation.name });
|
||||
});
|
||||
|
||||
it('should query a custom id relation', async () => {
|
||||
const { doc } = await client.findByID<Post>({ id: post.id });
|
||||
expect(doc?.customIdRelation).toMatchObject({ id: generatedCustomId });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createPost(overrides?: Partial<Post>) {
|
||||
return payload.create<Post>({ collection: slug, data: { title: 'title', ...overrides } });
|
||||
}
|
||||
|
||||
async function clearDocs(): Promise<void> {
|
||||
const allDocs = await payload.find<Post>({ collection: slug, limit: 100 });
|
||||
const ids = allDocs.docs.map((doc) => doc.id);
|
||||
await mapAsync(ids, async (id) => {
|
||||
await payload.delete({ collection: slug, id });
|
||||
});
|
||||
}
|
||||
83
test/relationships/payload-types.ts
Normal file
83
test/relationships/payload-types.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/* 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;
|
||||
defaultAccessRelation?: string | StrictAccess;
|
||||
chainedRelation?: string | ChainedRelation;
|
||||
maxDepthRelation?: string | Relation;
|
||||
customIdRelation?: string | CustomIdRelation;
|
||||
filteredRelation?: string | Relation;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "relation".
|
||||
*/
|
||||
export interface Relation {
|
||||
id: string;
|
||||
name?: string;
|
||||
disableRelation: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "strict-access".
|
||||
*/
|
||||
export interface StrictAccess {
|
||||
id: string;
|
||||
name?: string;
|
||||
disableRelation: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "chained-relation".
|
||||
*/
|
||||
export interface ChainedRelation {
|
||||
id: string;
|
||||
name?: string;
|
||||
relation?: string | ChainedRelation;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "custom-id-relation".
|
||||
*/
|
||||
export interface CustomIdRelation {
|
||||
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