diff --git a/nodemon.json b/nodemon.json index f7e53e057..2f7903bdb 100644 --- a/nodemon.json +++ b/nodemon.json @@ -4,7 +4,8 @@ "node_modules", "node_modules/**/node_modules", "src/admin", - "src/**/*.spec.ts" + "src/**/*.spec.ts", + "test/**/payload-types.ts" ], "watch": [ "src/**/*.ts", diff --git a/package.json b/package.json index c47ae45f1..ad12650ed 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "test:e2e:headed": "cross-env NODE_ENV=test DISABLE_LOGGING=true playwright test --headed", "test:e2e:debug": "cross-env PWDEBUG=1 NODE_ENV=test DISABLE_LOGGING=true playwright test", "test:components": "cross-env PAYLOAD_CONFIG_PATH=demo/payload.config.ts NODE_ENV=test jest --config=jest.components.config.js", + "generatetypes": "node ./test/dev/generateTypes.js", "clean": "rimraf dist", "release": "release-it", "release:patch": "release-it patch", diff --git a/test/dev/generateTypes.js b/test/dev/generateTypes.js new file mode 100644 index 000000000..ec2fa5d88 --- /dev/null +++ b/test/dev/generateTypes.js @@ -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'); +} diff --git a/test/e2e/buildConfig.ts b/test/e2e/buildConfig.ts index cece6dec5..d48f0d8c8 100644 --- a/test/e2e/buildConfig.ts +++ b/test/e2e/buildConfig.ts @@ -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): 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) => ({ diff --git a/test/helpers/rest.ts b/test/helpers/rest.ts index 3cdaa2366..af30cf52e 100644 --- a/test/helpers/rest.ts +++ b/test/helpers/rest.ts @@ -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 { const args = incomingArgs ?? { - email: 'dev@payloadcms.com', - password: 'test', + email: devUser.email, + password: devUser.password, collection: 'users', }; diff --git a/test/int/array-update/index.spec.ts b/test/int/array-update/index.spec.ts index 14d4c4597..a02d60abd 100644 --- a/test/int/array-update/index.spec.ts +++ b/test/int/array-update/index.spec.ts @@ -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({ 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({ collection, data: { array: [ @@ -82,7 +83,7 @@ describe('array-update', () => { }, }); - const updatedDoc = await payload.update({ + const updatedDoc = await payload.update({ 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); }); }); diff --git a/test/int/array-update/payload-types.ts b/test/int/array-update/payload-types.ts new file mode 100644 index 000000000..5dfead267 --- /dev/null +++ b/test/int/array-update/payload-types.ts @@ -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; +} diff --git a/test/int/buildConfig.ts b/test/int/buildConfig.ts index 1a445b8f4..793a30966 100644 --- a/test/int/buildConfig.ts +++ b/test/int/buildConfig.ts @@ -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): SanitizedConfig { - const baseConfig: Config = {}; + const baseConfig: Config = { + typescript: { + outputFile: process.env.PAYLOAD_TS_OUTPUT_PATH, + }, + }; if (process.env.NODE_ENV === 'test') { baseConfig.admin = { diff --git a/test/int/collections-graphql/index.spec.ts b/test/int/collections-graphql/index.spec.ts index e125fdd12..71142b2d4 100644 --- a/test/int/collections-graphql/index.spec.ts +++ b/test/int/collections-graphql/index.spec.ts @@ -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({ slug: collection, data: { title, diff --git a/test/int/collections-graphql/payload-types.ts b/test/int/collections-graphql/payload-types.ts new file mode 100644 index 000000000..3e93e6640 --- /dev/null +++ b/test/int/collections-graphql/payload-types.ts @@ -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; +} diff --git a/test/int/collections-rest/config.ts b/test/int/collections-rest/config.ts index abad5ea0e..27db8a7dd 100644 --- a/test/int/collections-rest/config.ts +++ b/test/int/collections-rest/config.ts @@ -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({ - collection: relationHasManySlug, + await payload.create({ + collection: 'users', + data: { + email: devUser.email, + password: devUser.password, + }, + }); + + const rel1 = await payload.create({ + collection: relationSlug, data: { name: 'name', }, }); + const rel2 = await payload.create({ + collection: relationSlug, + data: { + name: 'name2', + }, + }); - await payload.create({ + // Relation - hasMany + await payload.create({ collection: slug, data: { - title: 'title', + title: 'rel to hasMany', relationHasManyField: rel1.id, }, }); - }, + await payload.create({ + collection: slug, + data: { + title: 'rel to hasMany 2', + relationHasManyField: rel2.id, + }, + }); + // Relation - relationTo multi + await payload.create({ + collection: slug, + data: { + title: 'rel to multi', + relationMultiRelationTo: { + relationTo: relationSlug, + value: rel2.id, + }, + }, + }); + + // Relation - relationTo multi hasMany + await payload.create({ + collection: slug, + data: { + title: 'rel to multi hasMany', + relationMultiRelationToHasMany: [ + { + relationTo: relationSlug, + value: rel1.id, + }, + { + relationTo: relationSlug, + value: rel2.id, + }, + ], + }, + }); + }, }); diff --git a/test/int/collections-rest/index.spec.ts b/test/int/collections-rest/index.spec.ts index 5e25483c8..b4fdbd9cd 100644 --- a/test/int/collections-rest/index.spec.ts +++ b/test/int/collections-rest/index.spec.ts @@ -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({ + let post: Post; + let relation: Relation; + let relation2: Relation; + const nameToQuery = 'name'; + const nameToQuery2 = 'name'; + + beforeEach(async () => { + ({ doc: relation } = await client.create({ slug: relationSlug, data: { name: nameToQuery, }, - }); + })); - const post1 = await createPost({ + ({ doc: relation2 } = await client.create({ + slug: relationSlug, + data: { + name: nameToQuery2, + }, + })); + + post = await createPost({ relationField: relation.id, }); - await createPost(); - const { status, result } = await client.find({ - 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({ + 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({ + 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({ - 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({ 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({ + 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({ + 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({ + 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({ + 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({ 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) { const { doc } = await client.create({ data: { title: 'title', ...overrides } }); return doc; diff --git a/test/int/collections-rest/payload-types.ts b/test/int/collections-rest/payload-types.ts new file mode 100644 index 000000000..bc39874e9 --- /dev/null +++ b/test/int/collections-rest/payload-types.ts @@ -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; +}