From 2aee1505c7f481d02aefdc32785d010b55a900c0 Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Thu, 26 Dec 2024 17:41:35 +0200 Subject: [PATCH 1/4] init config --- test/joins/collections/SelfJoins.ts | 18 ++++++++++++++++ test/joins/config.ts | 2 ++ test/joins/payload-types.ts | 33 +++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 test/joins/collections/SelfJoins.ts diff --git a/test/joins/collections/SelfJoins.ts b/test/joins/collections/SelfJoins.ts new file mode 100644 index 00000000000..63fc4c43467 --- /dev/null +++ b/test/joins/collections/SelfJoins.ts @@ -0,0 +1,18 @@ +import type { CollectionConfig } from 'payload' + +export const SelfJoins: CollectionConfig = { + slug: 'self-joins', + fields: [ + { + name: 'rel', + type: 'relationship', + relationTo: 'self-joins', + }, + { + name: 'joins', + type: 'join', + on: 'rel', + collection: 'self-joins', + }, + ], +} diff --git a/test/joins/config.ts b/test/joins/config.ts index f0587237cb6..fd407ee4a27 100644 --- a/test/joins/config.ts +++ b/test/joins/config.ts @@ -6,6 +6,7 @@ import { Categories } from './collections/Categories.js' import { CategoriesVersions } from './collections/CategoriesVersions.js' import { HiddenPosts } from './collections/HiddenPosts.js' import { Posts } from './collections/Posts.js' +import { SelfJoins } from './collections/SelfJoins.js' import { Singular } from './collections/Singular.js' import { Uploads } from './collections/Uploads.js' import { Versions } from './collections/Versions.js' @@ -37,6 +38,7 @@ export default buildConfigWithDefaults({ Versions, CategoriesVersions, Singular, + SelfJoins, { slug: localizedPostsSlug, admin: { diff --git a/test/joins/payload-types.ts b/test/joins/payload-types.ts index 9cd322b5e66..241b99ec4e7 100644 --- a/test/joins/payload-types.ts +++ b/test/joins/payload-types.ts @@ -18,6 +18,7 @@ export interface Config { versions: Version; 'categories-versions': CategoriesVersion; singular: Singular; + 'self-joins': SelfJoin; 'localized-posts': LocalizedPost; 'localized-categories': LocalizedCategory; 'restricted-categories': RestrictedCategory; @@ -53,6 +54,9 @@ export interface Config { relatedVersions: 'versions'; relatedVersionsMany: 'versions'; }; + 'self-joins': { + joins: 'self-joins'; + }; 'localized-categories': { relatedPosts: 'localized-posts'; }; @@ -71,6 +75,7 @@ export interface Config { versions: VersionsSelect | VersionsSelect; 'categories-versions': CategoriesVersionsSelect | CategoriesVersionsSelect; singular: SingularSelect | SingularSelect; + 'self-joins': SelfJoinsSelect | SelfJoinsSelect; 'localized-posts': LocalizedPostsSelect | LocalizedPostsSelect; 'localized-categories': LocalizedCategoriesSelect | LocalizedCategoriesSelect; 'restricted-categories': RestrictedCategoriesSelect | RestrictedCategoriesSelect; @@ -355,6 +360,20 @@ export interface CategoriesVersion { createdAt: string; _status?: ('draft' | 'published') | null; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "self-joins". + */ +export interface SelfJoin { + id: string; + rel?: (string | null) | SelfJoin; + joins?: { + docs?: (string | SelfJoin)[] | null; + hasNextPage?: boolean | null; + } | null; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "localized-posts". @@ -467,6 +486,10 @@ export interface PayloadLockedDocument { relationTo: 'singular'; value: string | Singular; } | null) + | ({ + relationTo: 'self-joins'; + value: string | SelfJoin; + } | null) | ({ relationTo: 'localized-posts'; value: string | LocalizedPost; @@ -666,6 +689,16 @@ export interface SingularSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "self-joins_select". + */ +export interface SelfJoinsSelect { + rel?: T; + joins?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "localized-posts_select". From 087775d2ff7c43e586e997e5c57d11429b2dfd8b Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Thu, 26 Dec 2024 17:44:58 +0200 Subject: [PATCH 2/4] add test --- test/jest.setup.js | 2 +- test/joins/int.spec.ts | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/test/jest.setup.js b/test/jest.setup.js index 1b5d4da0f0d..109f12ebc2d 100644 --- a/test/jest.setup.js +++ b/test/jest.setup.js @@ -27,7 +27,7 @@ jest.spyOn(nodemailer, 'createTestAccount').mockImplementation(() => { if (!process.env.PAYLOAD_DATABASE) { // Mutate env so we can use conditions by DB adapter in tests properly without ignoring // eslint no-jest-conditions. - process.env.PAYLOAD_DATABASE = 'mongodb' + process.env.PAYLOAD_DATABASE = 'postgres' } generateDatabaseAdapter(process.env.PAYLOAD_DATABASE) diff --git a/test/joins/int.spec.ts b/test/joins/int.spec.ts index ff2cbe6e554..e25b541f439 100644 --- a/test/joins/int.spec.ts +++ b/test/joins/int.spec.ts @@ -1,4 +1,4 @@ -import type { Payload } from 'payload' +import type { Payload, TypeWithID } from 'payload' import path from 'path' import { getFileByPath } from 'payload' @@ -975,6 +975,15 @@ describe('Joins Field', () => { await payload.delete({ collection: categoriesSlug, where: { name: { equals: 'totalDocs' } } }) }) + + it('should self join', async () => { + const doc_1 = await payload.create({ collection: 'self-joins', data: {} }) + const doc_2 = await payload.create({ collection: 'self-joins', data: { rel: doc_1 }, depth: 0 }) + + const data = await payload.findByID({ collection: 'self-joins', id: doc_1.id, depth: 1 }) + + expect((data.joins.docs[0] as TypeWithID).id).toBe(doc_2.id) + }) }) async function createPost(overrides?: Partial, locale?: Config['locale']) { From 74fe960253e18a88cc8b39cdef2dad53ae2beb88 Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Thu, 26 Dec 2024 18:47:57 +0200 Subject: [PATCH 3/4] fix(db-postgres): joins to self `collection` --- packages/drizzle/src/find/traverseFields.ts | 26 ++++++++++++++++--- .../src/queries/buildAndOrConditions.ts | 5 +++- packages/drizzle/src/queries/buildOrderBy.ts | 9 ++++++- packages/drizzle/src/queries/buildQuery.ts | 6 ++++- packages/drizzle/src/queries/parseParams.ts | 25 ++++++++++++------ 5 files changed, 56 insertions(+), 15 deletions(-) diff --git a/packages/drizzle/src/find/traverseFields.ts b/packages/drizzle/src/find/traverseFields.ts index 9a989b6e776..3ed85d0c712 100644 --- a/packages/drizzle/src/find/traverseFields.ts +++ b/packages/drizzle/src/find/traverseFields.ts @@ -9,6 +9,8 @@ import type { BuildQueryJoinAliases, ChainedMethods, DrizzleAdapter } from '../t import type { Result } from './buildFindManyArgs.js' import buildQuery from '../queries/buildQuery.js' +import { getTableAlias } from '../queries/getTableAlias.js' +import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js' import { jsonAggBuildObject } from '../utilities/json.js' import { rawConstraint } from '../utilities/rawConstraint.js' import { chainMethods } from './chainMethods.js' @@ -385,12 +387,22 @@ export const traverseFields = ({ } } + const columnName = `${path.replaceAll('.', '_')}${field.name}` + + const subQueryAlias = `${columnName}_alias` + + const { newAliasTable } = getTableAlias({ + adapter, + tableName: joinCollectionTableName, + }) + const { orderBy, selectFields, where: subQueryWhere, } = buildQuery({ adapter, + aliasTable: newAliasTable, fields, joins, locale, @@ -418,15 +430,21 @@ export const traverseFields = ({ const db = adapter.drizzle as LibSQLDatabase - const columnName = `${path.replaceAll('.', '_')}${field.name}` + for (let key in selectFields) { + const val = selectFields[key] - const subQueryAlias = `${columnName}_alias` + if (val.table && getNameFromDrizzleTable(val.table) === joinCollectionTableName) { + delete selectFields[key] + key = key.split('.').pop() + selectFields[key] = newAliasTable[key] + } + } const subQuery = chainMethods({ methods: chainedMethods, query: db .select(selectFields as any) - .from(adapter.tables[joinCollectionTableName]) + .from(newAliasTable) .where(subQueryWhere) .orderBy(() => orderBy.map(({ column, order }) => order(column))), }).as(subQueryAlias) @@ -440,7 +458,7 @@ export const traverseFields = ({ }), }), }) - .from(sql`${subQuery}`)}`.as(columnName) + .from(sql`${subQuery}`)}`.as(subQueryAlias) break } diff --git a/packages/drizzle/src/queries/buildAndOrConditions.ts b/packages/drizzle/src/queries/buildAndOrConditions.ts index 7c242630a31..c59687c5c1e 100644 --- a/packages/drizzle/src/queries/buildAndOrConditions.ts +++ b/packages/drizzle/src/queries/buildAndOrConditions.ts @@ -1,4 +1,4 @@ -import type { SQL } from 'drizzle-orm' +import type { SQL, Table } from 'drizzle-orm' import type { FlattenedField, Where } from 'payload' import type { DrizzleAdapter, GenericColumn } from '../types.js' @@ -8,6 +8,7 @@ import { parseParams } from './parseParams.js' export function buildAndOrConditions({ adapter, + aliasTable, fields, joins, locale, @@ -17,6 +18,7 @@ export function buildAndOrConditions({ where, }: { adapter: DrizzleAdapter + aliasTable?: Table collectionSlug?: string fields: FlattenedField[] globalSlug?: string @@ -36,6 +38,7 @@ export function buildAndOrConditions({ if (typeof condition === 'object') { const result = parseParams({ adapter, + aliasTable, fields, joins, locale, diff --git a/packages/drizzle/src/queries/buildOrderBy.ts b/packages/drizzle/src/queries/buildOrderBy.ts index b6cd8c66ac4..81c6e6ab640 100644 --- a/packages/drizzle/src/queries/buildOrderBy.ts +++ b/packages/drizzle/src/queries/buildOrderBy.ts @@ -1,3 +1,4 @@ +import type { Table } from 'drizzle-orm' import type { FlattenedField, Sort } from 'payload' import { asc, desc } from 'drizzle-orm' @@ -5,10 +6,12 @@ import { asc, desc } from 'drizzle-orm' import type { DrizzleAdapter, GenericColumn } from '../types.js' import type { BuildQueryJoinAliases, BuildQueryResult } from './buildQuery.js' +import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js' import { getTableColumnFromPath } from './getTableColumnFromPath.js' type Args = { adapter: DrizzleAdapter + aliasTable?: Table fields: FlattenedField[] joins: BuildQueryJoinAliases locale?: string @@ -22,6 +25,7 @@ type Args = { */ export const buildOrderBy = ({ adapter, + aliasTable, fields, joins, locale, @@ -68,7 +72,10 @@ export const buildOrderBy = ({ }) if (sortTable?.[sortTableColumnName]) { orderBy.push({ - column: sortTable[sortTableColumnName], + column: + aliasTable && tableName === getNameFromDrizzleTable(sortTable) + ? aliasTable[sortTableColumnName] + : sortTable[sortTableColumnName], order: sortDirection === 'asc' ? asc : desc, }) diff --git a/packages/drizzle/src/queries/buildQuery.ts b/packages/drizzle/src/queries/buildQuery.ts index 0db922a5793..7777355a36e 100644 --- a/packages/drizzle/src/queries/buildQuery.ts +++ b/packages/drizzle/src/queries/buildQuery.ts @@ -1,4 +1,4 @@ -import type { asc, desc, SQL } from 'drizzle-orm' +import type { asc, desc, SQL, Table } from 'drizzle-orm' import type { PgTableWithColumns } from 'drizzle-orm/pg-core' import type { FlattenedField, Sort, Where } from 'payload' @@ -15,6 +15,7 @@ export type BuildQueryJoinAliases = { type BuildQueryArgs = { adapter: DrizzleAdapter + aliasTable?: Table fields: FlattenedField[] joins?: BuildQueryJoinAliases locale?: string @@ -35,6 +36,7 @@ export type BuildQueryResult = { } const buildQuery = function buildQuery({ adapter, + aliasTable, fields, joins = [], locale, @@ -49,6 +51,7 @@ const buildQuery = function buildQuery({ const orderBy = buildOrderBy({ adapter, + aliasTable, fields, joins, locale, @@ -62,6 +65,7 @@ const buildQuery = function buildQuery({ if (incomingWhere && Object.keys(incomingWhere).length > 0) { where = parseParams({ adapter, + aliasTable, fields, joins, locale, diff --git a/packages/drizzle/src/queries/parseParams.ts b/packages/drizzle/src/queries/parseParams.ts index ed98eeac100..352c6a0ef95 100644 --- a/packages/drizzle/src/queries/parseParams.ts +++ b/packages/drizzle/src/queries/parseParams.ts @@ -1,4 +1,4 @@ -import type { SQL } from 'drizzle-orm' +import type { SQL, Table } from 'drizzle-orm' import type { FlattenedField, Operator, Where } from 'payload' import { and, isNotNull, isNull, ne, notInArray, or, sql } from 'drizzle-orm' @@ -9,12 +9,14 @@ import { validOperators } from 'payload/shared' import type { DrizzleAdapter, GenericColumn } from '../types.js' import type { BuildQueryJoinAliases } from './buildQuery.js' +import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js' import { buildAndOrConditions } from './buildAndOrConditions.js' import { getTableColumnFromPath } from './getTableColumnFromPath.js' import { sanitizeQueryValue } from './sanitizeQueryValue.js' type Args = { adapter: DrizzleAdapter + aliasTable?: Table fields: FlattenedField[] joins: BuildQueryJoinAliases locale: string @@ -26,6 +28,7 @@ type Args = { export function parseParams({ adapter, + aliasTable, fields, joins, locale, @@ -51,6 +54,7 @@ export function parseParams({ if (Array.isArray(condition)) { const builtConditions = buildAndOrConditions({ adapter, + aliasTable, fields, joins, locale, @@ -83,6 +87,7 @@ export function parseParams({ table, } = getTableColumnFromPath({ adapter, + aliasTable, collectionPath: relationOrPath, fields, joins, @@ -261,12 +266,18 @@ export function parseParams({ break } + const resolvedColumn = + rawColumn || + (aliasTable && tableName === getNameFromDrizzleTable(table) + ? aliasTable[columnName] + : table[columnName]) + if (queryOperator === 'not_equals' && queryValue !== null) { constraints.push( or( - isNull(rawColumn || table[columnName]), + isNull(resolvedColumn), /* eslint-disable @typescript-eslint/no-explicit-any */ - ne(rawColumn || table[columnName], queryValue), + ne(resolvedColumn, queryValue), ), ) break @@ -288,12 +299,12 @@ export function parseParams({ } if (operator === 'equals' && queryValue === null) { - constraints.push(isNull(rawColumn || table[columnName])) + constraints.push(isNull(resolvedColumn)) break } if (operator === 'not_equals' && queryValue === null) { - constraints.push(isNotNull(rawColumn || table[columnName])) + constraints.push(isNotNull(resolvedColumn)) break } @@ -330,9 +341,7 @@ export function parseParams({ break } - constraints.push( - adapter.operators[queryOperator](rawColumn || table[columnName], queryValue), - ) + constraints.push(adapter.operators[queryOperator](resolvedColumn, queryValue)) } } } From d5dde5848e785fa0f8e740501ef43f8477984628 Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Thu, 26 Dec 2024 18:52:41 +0200 Subject: [PATCH 4/4] reset adapter --- test/jest.setup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jest.setup.js b/test/jest.setup.js index 109f12ebc2d..1b5d4da0f0d 100644 --- a/test/jest.setup.js +++ b/test/jest.setup.js @@ -27,7 +27,7 @@ jest.spyOn(nodemailer, 'createTestAccount').mockImplementation(() => { if (!process.env.PAYLOAD_DATABASE) { // Mutate env so we can use conditions by DB adapter in tests properly without ignoring // eslint no-jest-conditions. - process.env.PAYLOAD_DATABASE = 'postgres' + process.env.PAYLOAD_DATABASE = 'mongodb' } generateDatabaseAdapter(process.env.PAYLOAD_DATABASE)