diff --git a/packages/db-postgres/package.json b/packages/db-postgres/package.json index b10c56e26..5d60748fb 100644 --- a/packages/db-postgres/package.json +++ b/packages/db-postgres/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/db-postgres", - "version": "0.1.6", + "version": "0.1.7", "description": "The officially supported Postgres database adapter for Payload", "repository": "https://github.com/payloadcms/payload", "license": "MIT", diff --git a/packages/db-postgres/src/queries/getTableColumnFromPath.ts b/packages/db-postgres/src/queries/getTableColumnFromPath.ts index 9c0a0143e..13f37c8a4 100644 --- a/packages/db-postgres/src/queries/getTableColumnFromPath.ts +++ b/packages/db-postgres/src/queries/getTableColumnFromPath.ts @@ -34,12 +34,14 @@ type Args = { aliasTable?: GenericTable collectionPath: string columnPrefix?: string + constraintPath?: string constraints?: Constraint[] fields: (Field | TabAsField)[] joinAliases: BuildQueryJoinAliases joins: BuildQueryJoins locale?: string pathSegments: string[] + rootTableName?: string selectFields: Record tableName: string } @@ -53,17 +55,22 @@ export const getTableColumnFromPath = ({ aliasTable, collectionPath, columnPrefix = '', + constraintPath: incomingConstraintPath, constraints = [], fields, joinAliases, joins, locale: incomingLocale, pathSegments: incomingSegments, + rootTableName: incomingRootTableName, selectFields, tableName, }: Args): TableColumn => { const fieldPath = incomingSegments[0] let locale = incomingLocale + const rootTableName = incomingRootTableName || tableName + let constraintPath = incomingConstraintPath || '' + const field = flattenTopLevelFields(fields as Field[]).find( (fieldToFind) => fieldAffectsData(fieldToFind) && fieldToFind.name === fieldPath, ) as Field | TabAsField @@ -105,6 +112,7 @@ export const getTableColumnFromPath = ({ aliasTable, collectionPath, columnPrefix, + constraintPath, constraints, fields: field.tabs.map((tab) => ({ ...tab, @@ -114,6 +122,7 @@ export const getTableColumnFromPath = ({ joins, locale, pathSegments: pathSegments.slice(1), + rootTableName, selectFields, tableName: newTableName, }) @@ -125,12 +134,14 @@ export const getTableColumnFromPath = ({ aliasTable, collectionPath, columnPrefix: `${columnPrefix}${field.name}_`, + constraintPath, constraints, fields: field.fields, joinAliases, joins, locale, pathSegments: pathSegments.slice(1), + rootTableName, selectFields, tableName: newTableName, }) @@ -140,12 +151,14 @@ export const getTableColumnFromPath = ({ aliasTable, collectionPath, columnPrefix, + constraintPath, constraints, fields: field.fields, joinAliases, joins, locale, pathSegments: pathSegments.slice(1), + rootTableName, selectFields, tableName: newTableName, }) @@ -172,12 +185,14 @@ export const getTableColumnFromPath = ({ aliasTable, collectionPath, columnPrefix: `${columnPrefix}${field.name}_`, + constraintPath, constraints, fields: field.fields, joinAliases, joins, locale, pathSegments: pathSegments.slice(1), + rootTableName, selectFields, tableName: newTableName, }) @@ -185,6 +200,7 @@ export const getTableColumnFromPath = ({ case 'array': { newTableName = `${tableName}_${toSnakeCase(field.name)}` + constraintPath = `${constraintPath}${field.name}.%.` if (locale && field.localized && adapter.payload.config.localization) { joins[newTableName] = and( eq(adapter.tables[tableName].id, adapter.tables[newTableName]._parentID), @@ -206,12 +222,14 @@ export const getTableColumnFromPath = ({ return getTableColumnFromPath({ adapter, collectionPath, + constraintPath, constraints, fields: field.fields, joinAliases, joins, locale, pathSegments: pathSegments.slice(1), + rootTableName, selectFields, tableName: newTableName, }) @@ -229,12 +247,14 @@ export const getTableColumnFromPath = ({ result = getTableColumnFromPath({ adapter, collectionPath, + constraintPath: '', constraints: blockConstraints, fields: block.fields, joinAliases, joins, locale, pathSegments: pathSegments.slice(1), + rootTableName, selectFields: blockSelectFields, tableName: newTableName, }) @@ -283,9 +303,8 @@ export const getTableColumnFromPath = ({ case 'relationship': case 'upload': { let relationshipFields - const relationTableName = `${tableName}_rels` + const relationTableName = `${rootTableName}_rels` const newCollectionPath = pathSegments.slice(1).join('.') - const aliasRelationshipTableName = uuid() const aliasRelationshipTable = alias( adapter.tables[relationTableName], @@ -295,7 +314,7 @@ export const getTableColumnFromPath = ({ // Join in the relationships table joinAliases.push({ condition: eq( - (aliasTable || adapter.tables[tableName]).id, + (aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent, ), table: aliasRelationshipTable, @@ -306,7 +325,7 @@ export const getTableColumnFromPath = ({ constraints.push({ columnName: 'path', table: aliasRelationshipTable, - value: field.name, + value: `${constraintPath}${field.name}`, }) let newAliasTable @@ -368,6 +387,7 @@ export const getTableColumnFromPath = ({ joins, locale, pathSegments: pathSegments.slice(1), + rootTableName: newTableName, selectFields, tableName: newTableName, }) diff --git a/packages/db-postgres/src/queries/parseParams.ts b/packages/db-postgres/src/queries/parseParams.ts index 188bbee44..127b358b6 100644 --- a/packages/db-postgres/src/queries/parseParams.ts +++ b/packages/db-postgres/src/queries/parseParams.ts @@ -100,7 +100,11 @@ export async function parseParams({ const val = where[relationOrPath][operator] queryConstraints.forEach(({ columnName: col, table: constraintTable, value }) => { - constraints.push(operatorMap.equals(constraintTable[col], value)) + if (typeof value === 'string' && value.indexOf('%') > -1) { + constraints.push(operatorMap.like(constraintTable[col], value)) + } else { + constraints.push(operatorMap.equals(constraintTable[col], value)) + } }) if (['json', 'richText'].includes(field.type) && Array.isArray(pathSegments)) { diff --git a/test/fields/collections/Relationship/index.ts b/test/fields/collections/Relationship/index.ts index a9587176d..157cab3f9 100644 --- a/test/fields/collections/Relationship/index.ts +++ b/test/fields/collections/Relationship/index.ts @@ -3,63 +3,81 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collecti export const relationshipFieldsSlug = 'relationship-fields' const RelationshipFields: CollectionConfig = { - slug: relationshipFieldsSlug, fields: [ + { + name: 'text', + type: 'text', + }, { name: 'relationship', - type: 'relationship', relationTo: ['text-fields', 'array-fields'], required: true, + type: 'relationship', }, { name: 'relationToSelf', - type: 'relationship', relationTo: relationshipFieldsSlug, + type: 'relationship', }, { name: 'relationToSelfSelectOnly', - type: 'relationship', - relationTo: relationshipFieldsSlug, admin: { allowCreate: false, }, + relationTo: relationshipFieldsSlug, + type: 'relationship', }, { name: 'relationWithDynamicDefault', - type: 'relationship', + defaultValue: ({ user }) => user?.id, relationTo: 'users', - defaultValue: ({ user }) => user.id, + type: 'relationship', }, { name: 'relationHasManyWithDynamicDefault', - type: 'relationship', + defaultValue: ({ user }) => + user + ? { + relationTo: 'users', + value: user.id, + } + : undefined, relationTo: ['users'], - defaultValue: ({ user }) => ({ - relationTo: 'users', - value: user.id, - }), + type: 'relationship', }, { name: 'relationshipWithMin', - type: 'relationship', - relationTo: 'text-fields', hasMany: true, minRows: 2, + relationTo: 'text-fields', + type: 'relationship', }, { name: 'relationshipWithMax', - type: 'relationship', - relationTo: 'text-fields', hasMany: true, maxRows: 2, + relationTo: 'text-fields', + type: 'relationship', }, { name: 'relationshipHasMany', - type: 'relationship', - relationTo: 'text-fields', hasMany: true, + relationTo: 'text-fields', + type: 'relationship', + }, + { + name: 'array', + fields: [ + { + name: 'relationship', + relationTo: 'text-fields', + type: 'relationship', + }, + ], + type: 'array', }, ], + slug: relationshipFieldsSlug, } export default RelationshipFields diff --git a/test/fields/config.ts b/test/fields/config.ts index fa64a5d92..94c4371e8 100644 --- a/test/fields/config.ts +++ b/test/fields/config.ts @@ -44,18 +44,18 @@ export default buildConfigWithDefaults({ collections: [ LexicalFields, { - slug: 'users', - auth: true, admin: { useAsTitle: 'email', }, + auth: true, fields: [ { name: 'canViewConditionalField', - type: 'checkbox', defaultValue: true, + type: 'checkbox', }, ], + slug: 'users', }, ArrayFields, BlockFields, @@ -81,8 +81,8 @@ export default buildConfigWithDefaults({ ], localization: { defaultLocale: 'en', - locales: ['en', 'es'], fallback: true, + locales: ['en', 'es'], }, onInit: async (payload) => { await payload.create({ diff --git a/test/fields/e2e.spec.ts b/test/fields/e2e.spec.ts index 81c75aea8..0e35a9e0d 100644 --- a/test/fields/e2e.spec.ts +++ b/test/fields/e2e.spec.ts @@ -1093,7 +1093,7 @@ describe('fields', () => { .locator('#field-relationship .relationship-add-new__relation-button--text-fields') .click() - const textField = page.locator('#field-text') + const textField = page.locator('.drawer__content #field-text') const textValue = 'hello' await textField.fill(textValue) @@ -1217,7 +1217,7 @@ describe('fields', () => { .locator('#field-relationship .relationship-add-new__relation-button--text-fields') .click() - await page.locator('#field-text').fill('something') + await page.locator('.drawer__content #field-text').fill('something') await page.locator('[id^=doc-drawer_text-fields_1_] #action-save').click() await expect(page.locator('.Toastify')).toContainText('successfully') @@ -1290,7 +1290,7 @@ describe('fields', () => { await page.getByRole('button', { name: 'Edit Seeded text document' }).click() // Fill 'text' field of 'Seeded text document' - await page.locator('#field-text').fill('some updated text value') + await page.locator('.drawer__content #field-text').fill('some updated text value') // Save drawer (not parent page) with hotkey await saveDocHotkeyAndAssert(page) diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index 82dfc111a..f93fa374c 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -21,6 +21,7 @@ import { } from './collections/Group' import { defaultNumber, numberDoc } from './collections/Number' import { pointDoc } from './collections/Point' +import { relationshipFieldsSlug } from './collections/Relationship' import { tabsDoc } from './collections/Tabs' import { localizedTextValue, @@ -73,6 +74,122 @@ describe('Fields', () => { }) }) + describe('relationship', () => { + let textDoc + let otherTextDoc + let selfReferencing + let parent + let child + let grandChild + let relationshipInArray + const textDocText = 'text document' + const otherTextDocText = 'alt text' + const relationshipText = 'relationship text' + + beforeAll(async () => { + textDoc = await payload.create({ + collection: 'text-fields', + data: { + text: textDocText, + }, + }) + otherTextDoc = await payload.create({ + collection: 'text-fields', + data: { + text: otherTextDocText, + }, + }) + const relationship = { relationTo: 'text-fields', value: textDoc.id } + parent = await payload.create({ + collection: relationshipFieldsSlug, + data: { + relationship, + text: relationshipText, + }, + }) + + child = await payload.create({ + collection: relationshipFieldsSlug, + data: { + relationToSelf: parent.id, + relationship, + text: relationshipText, + }, + }) + + grandChild = await payload.create({ + collection: relationshipFieldsSlug, + data: { + relationToSelf: child.id, + relationship, + text: relationshipText, + }, + }) + + selfReferencing = await payload.create({ + collection: relationshipFieldsSlug, + data: { + relationship, + text: relationshipText, + }, + }) + + relationshipInArray = await payload.create({ + collection: relationshipFieldsSlug, + data: { + array: [ + { + relationship: otherTextDoc.id, + }, + ], + relationship, + }, + }) + }) + + it('should query parent self-reference', async () => { + const childResult = await payload.find({ + collection: relationshipFieldsSlug, + where: { + relationToSelf: { equals: parent.id }, + }, + }) + + const grandChildResult = await payload.find({ + collection: relationshipFieldsSlug, + where: { + relationToSelf: { equals: child.id }, + }, + }) + + const anyChildren = await payload.find({ + collection: relationshipFieldsSlug, + }) + const allChildren = await payload.find({ + collection: relationshipFieldsSlug, + where: { + 'relationToSelf.text': { equals: relationshipText }, + }, + }) + + expect(childResult.docs[0].id).toStrictEqual(child.id) + expect(grandChildResult.docs[0].id).toStrictEqual(grandChild.id) + expect(allChildren.docs).toHaveLength(2) + }) + + it('should query relationship inside array', async () => { + const result = await payload.find({ + collection: relationshipFieldsSlug, + where: { + 'array.relationship.text': { equals: otherTextDocText }, + }, + }) + + expect(result.docs).toHaveLength(1) + expect(result.docs[0]).toMatchObject(relationshipInArray) + }) + }) + describe('timestamps', () => { const tenMinutesAgo = new Date(Date.now() - 1000 * 60 * 10) let doc