diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9f6ade00a2..8707c9b718 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,9 +2,9 @@ name: build on: pull_request: - types: [opened, reopened, synchronize] + types: [ opened, reopened, synchronize ] push: - branches: ['main'] + branches: [ 'main' ] jobs: changes: @@ -15,25 +15,25 @@ jobs: needs_build: ${{ steps.filter.outputs.needs_build }} templates: ${{ steps.filter.outputs.templates }} steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 25 - - uses: dorny/paths-filter@v2 - id: filter - with: - filters: | - needs_build: - - '.github/workflows/**' - - 'packages/**' - - 'test/**' - - 'pnpm-lock.yaml' - - 'package.json' - templates: - - 'templates/**' - - name: Log all filter results - run: | - echo "needs_build: ${{ steps.filter.outputs.needs_build }}" - echo "templates: ${{ steps.filter.outputs.templates }}" + - uses: actions/checkout@v4 + with: + fetch-depth: 25 + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + needs_build: + - '.github/workflows/**' + - 'packages/**' + - 'test/**' + - 'pnpm-lock.yaml' + - 'package.json' + templates: + - 'templates/**' + - name: Log all filter results + run: | + echo "needs_build: ${{ steps.filter.outputs.needs_build }}" + echo "templates: ${{ steps.filter.outputs.templates }}" core-build: needs: changes @@ -85,7 +85,7 @@ jobs: strategy: fail-fast: false matrix: - database: [mongoose, postgres, supabase] + database: [ mongoose, postgres, postgres-uuid, supabase ] env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -116,7 +116,7 @@ jobs: postgresql db: ${{ env.POSTGRES_DB }} postgresql user: ${{ env.POSTGRES_USER }} postgresql password: ${{ env.POSTGRES_PASSWORD }} - if: matrix.database == 'postgres' + if: matrix.database == 'postgres' || matrix.database == 'postgres-uuid' - name: Install Supabase CLI uses: supabase/setup-cli@v1 @@ -132,14 +132,14 @@ jobs: - name: Wait for PostgreSQL run: sleep 30 - if: matrix.database == 'postgres' + if: matrix.database == 'postgres' || matrix.database == 'postgres-uuid' - name: Configure PostgreSQL run: | psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE ROLE runner SUPERUSER LOGIN;" psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "SELECT version();" echo "POSTGRES_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV - if: matrix.database == 'postgres' + if: matrix.database == 'postgres' || matrix.database == 'postgres-uuid' - name: Configure Supabase run: | @@ -162,7 +162,7 @@ jobs: strategy: fail-fast: false matrix: - part: [1/8, 2/8, 3/8, 4/8, 5/8, 6/8, 7/8, 8/8] + part: [ 1/8, 2/8, 3/8, 4/8, 5/8, 6/8, 7/8, 8/8 ] steps: - name: Use Node.js 18 @@ -310,7 +310,7 @@ jobs: strategy: fail-fast: false matrix: - template: [blank, website, ecommerce] + template: [ blank, website, ecommerce ] steps: - uses: actions/checkout@v4 diff --git a/packages/db-postgres/src/index.ts b/packages/db-postgres/src/index.ts index bab35d9e6a..aebe1e34cb 100644 --- a/packages/db-postgres/src/index.ts +++ b/packages/db-postgres/src/index.ts @@ -42,7 +42,7 @@ export type { MigrateDownArgs, MigrateUpArgs } from './types' export function postgresAdapter(args: Args): PostgresAdapterResult { function adapter({ payload }: { payload: Payload }) { const migrationDir = findMigrationDir(args.migrationDir) - + const idType = args.idType || 'serial' return createDatabaseAdapter({ name: 'postgres', @@ -50,6 +50,7 @@ export function postgresAdapter(args: Args): PostgresAdapterResult { drizzle: undefined, enums: {}, fieldConstraints: {}, + idType, logger: args.logger, pool: undefined, poolOptions: args.pool, @@ -68,7 +69,10 @@ export function postgresAdapter(args: Args): PostgresAdapterResult { createGlobalVersion, createMigration, createVersion, - defaultIDType: 'number', + /** + * This represents how a default ID is treated in Payload as were a field type + */ + defaultIDType: idType === 'serial' ? 'number' : 'text', deleteMany, deleteOne, deleteVersions, diff --git a/packages/db-postgres/src/init.ts b/packages/db-postgres/src/init.ts index 233508144c..777dae82bf 100644 --- a/packages/db-postgres/src/init.ts +++ b/packages/db-postgres/src/init.ts @@ -9,7 +9,6 @@ import toSnakeCase from 'to-snake-case' import type { PostgresAdapter } from './types' import { buildTable } from './schema/build' -import { getConfigIDType } from './schema/getConfigIDType' export const init: Init = async function init(this: PostgresAdapter) { if (this.payload.config.localization) { @@ -24,9 +23,9 @@ export const init: Init = async function init(this: PostgresAdapter) { buildTable({ adapter: this, - buildTexts: true, buildNumbers: true, buildRelationships: true, + buildTexts: true, disableNotNull: !!collection?.versions?.drafts, disableUnique: false, fields: collection.fields, @@ -38,13 +37,11 @@ export const init: Init = async function init(this: PostgresAdapter) { const versionsTableName = `_${tableName}_v` const versionFields = buildVersionCollectionFields(collection) - const versionsParentIDColType = getConfigIDType(collection.fields) - buildTable({ adapter: this, - buildTexts: true, buildNumbers: true, buildRelationships: true, + buildTexts: true, disableNotNull: !!collection.versions?.drafts, disableUnique: true, fields: versionFields, @@ -59,9 +56,9 @@ export const init: Init = async function init(this: PostgresAdapter) { buildTable({ adapter: this, - buildTexts: true, buildNumbers: true, buildRelationships: true, + buildTexts: true, disableNotNull: !!global?.versions?.drafts, disableUnique: false, fields: global.fields, @@ -75,9 +72,9 @@ export const init: Init = async function init(this: PostgresAdapter) { buildTable({ adapter: this, - buildTexts: true, buildNumbers: true, buildRelationships: true, + buildTexts: true, disableNotNull: !!global.versions?.drafts, disableUnique: true, fields: versionFields, diff --git a/packages/db-postgres/src/queries/getTableColumnFromPath.ts b/packages/db-postgres/src/queries/getTableColumnFromPath.ts index 2f01c0d906..258e3f361e 100644 --- a/packages/db-postgres/src/queries/getTableColumnFromPath.ts +++ b/packages/db-postgres/src/queries/getTableColumnFromPath.ts @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ import type { SQL } from 'drizzle-orm' -import type { Field, FieldAffectingData, TabAsField } from 'payload/types' +import type { Field, FieldAffectingData, NumberField, TabAsField, TextField } from 'payload/types' import { and, eq, like, sql } from 'drizzle-orm' import { alias } from 'drizzle-orm/pg-core' @@ -88,8 +88,8 @@ export const getTableColumnFromPath = ({ constraints, field: { name: 'id', - type: 'number', - }, + type: adapter.idType === 'uuid' ? 'text' : 'number', + } as TextField | NumberField, table: adapter.tables[newTableName], } } diff --git a/packages/db-postgres/src/queries/parseParams.ts b/packages/db-postgres/src/queries/parseParams.ts index ef19156496..53f78b46f4 100644 --- a/packages/db-postgres/src/queries/parseParams.ts +++ b/packages/db-postgres/src/queries/parseParams.ts @@ -169,6 +169,7 @@ export async function parseParams({ } const sanitizedQueryValue = sanitizeQueryValue({ + adapter, field, operator, relationOrPath, diff --git a/packages/db-postgres/src/queries/sanitizeQueryValue.ts b/packages/db-postgres/src/queries/sanitizeQueryValue.ts index f35061a1bd..56d8fc3add 100644 --- a/packages/db-postgres/src/queries/sanitizeQueryValue.ts +++ b/packages/db-postgres/src/queries/sanitizeQueryValue.ts @@ -2,7 +2,10 @@ import { APIError } from 'payload/errors' import { type Field, type TabAsField, fieldAffectsData } from 'payload/types' import { createArrayFromCommaDelineated } from 'payload/utilities' +import type { PostgresAdapter } from '../types' + type SanitizeQueryValueArgs = { + adapter: PostgresAdapter field: Field | TabAsField operator: string relationOrPath: string @@ -10,6 +13,7 @@ type SanitizeQueryValueArgs = { } export const sanitizeQueryValue = ({ + adapter, field, operator: operatorArg, relationOrPath, @@ -27,8 +31,10 @@ export const sanitizeQueryValue = ({ ) { const allPossibleIDTypes: (number | string)[] = [] formattedValue.forEach((val) => { - if (typeof val === 'string') { + if (adapter.idType !== 'uuid' && typeof val === 'string') { allPossibleIDTypes.push(val, parseInt(val)) + } else if (typeof val === 'string') { + allPossibleIDTypes.push(val) } else { allPossibleIDTypes.push(val, String(val)) } diff --git a/packages/db-postgres/src/schema/build.ts b/packages/db-postgres/src/schema/build.ts index c2ed66c438..2905d8684b 100644 --- a/packages/db-postgres/src/schema/build.ts +++ b/packages/db-postgres/src/schema/build.ts @@ -17,10 +17,10 @@ import { import { fieldAffectsData } from 'payload/types' import toSnakeCase from 'to-snake-case' -import type { GenericColumns, GenericTable, PostgresAdapter } from '../types' +import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types' -import { getConfigIDType } from './getConfigIDType' import { parentIDColumnMap } from './parentIDColumnMap' +import { setColumnID } from './setColumnID' import { traverseFields } from './traverseFields' type Args = { @@ -89,15 +89,8 @@ export const buildTable = ({ // Drizzle relations const relationsToBuild: Map = new Map() - const idColType = getConfigIDType(fields) + const idColType: IDType = setColumnID({ adapter, columns, fields }) - const idColTypeMap = { - integer: serial, - numeric, - varchar, - } - - columns.id = idColTypeMap[idColType]('id').primaryKey() ;({ hasLocalizedField, hasLocalizedManyNumberField, @@ -300,7 +293,7 @@ export const buildTable = ({ relationships.forEach((relationTo) => { const formattedRelationTo = toSnakeCase(relationTo) - let colType = 'integer' + let colType = adapter.idType === 'uuid' ? 'uuid' : 'integer' const relatedCollectionCustomID = adapter.payload.collections[ relationTo ].config.fields.find((field) => fieldAffectsData(field) && field.name === 'id') diff --git a/packages/db-postgres/src/schema/getConfigIDType.ts b/packages/db-postgres/src/schema/getConfigIDType.ts deleted file mode 100644 index 5f21d58f65..0000000000 --- a/packages/db-postgres/src/schema/getConfigIDType.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { type Field, fieldAffectsData } from 'payload/types' - -export const getConfigIDType = (fields: Field[]): string => { - const idField = fields.find((field) => fieldAffectsData(field) && field.name === 'id') - - if (idField) { - if (idField.type === 'number') { - return 'numeric' - } - - if (idField.type === 'text') { - return 'varchar' - } - } - - return 'integer' -} diff --git a/packages/db-postgres/src/schema/parentIDColumnMap.ts b/packages/db-postgres/src/schema/parentIDColumnMap.ts index eba60e8d08..a82cb98f8a 100644 --- a/packages/db-postgres/src/schema/parentIDColumnMap.ts +++ b/packages/db-postgres/src/schema/parentIDColumnMap.ts @@ -1,7 +1,13 @@ -import { integer, numeric, varchar } from 'drizzle-orm/pg-core' +import { integer, numeric, uuid, varchar } from 'drizzle-orm/pg-core' -export const parentIDColumnMap = { +import type { IDType } from '../types' + +export const parentIDColumnMap: Record< + IDType, + typeof integer | typeof numeric | typeof uuid | typeof varchar +> = { integer, numeric, + uuid, varchar, } diff --git a/packages/db-postgres/src/schema/setColumnID.ts b/packages/db-postgres/src/schema/setColumnID.ts new file mode 100644 index 0000000000..74e6bc4c84 --- /dev/null +++ b/packages/db-postgres/src/schema/setColumnID.ts @@ -0,0 +1,33 @@ +import type { PgColumnBuilder } from 'drizzle-orm/pg-core' + +import { numeric, serial, uuid, varchar } from 'drizzle-orm/pg-core' +import { type Field, fieldAffectsData } from 'payload/types' +import { flattenTopLevelFields } from 'payload/utilities' + +import type { IDType, PostgresAdapter } from '../types' + +type Args = { adapter: PostgresAdapter; columns: Record; fields: Field[] } +export const setColumnID = ({ adapter, columns, fields }: Args): IDType => { + const idField = flattenTopLevelFields(fields).find( + (field) => fieldAffectsData(field) && field.name === 'id', + ) + if (idField) { + if (idField.type === 'number') { + columns.id = numeric('id').primaryKey() + return 'numeric' + } + + if (idField.type === 'text') { + columns.id = varchar('id').primaryKey() + return 'varchar' + } + } + + if (adapter.idType === 'uuid') { + columns.id = uuid('id').defaultRandom().primaryKey() + return 'uuid' + } + + columns.id = serial('id').primaryKey() + return 'integer' +} diff --git a/packages/db-postgres/src/schema/traverseFields.ts b/packages/db-postgres/src/schema/traverseFields.ts index 35093bb401..632a820834 100644 --- a/packages/db-postgres/src/schema/traverseFields.ts +++ b/packages/db-postgres/src/schema/traverseFields.ts @@ -6,6 +6,7 @@ import type { Field, TabAsField } from 'payload/types' import { relations } from 'drizzle-orm' import { PgNumericBuilder, + PgUUIDBuilder, PgVarcharBuilder, boolean, index, @@ -21,7 +22,7 @@ import { InvalidConfiguration } from 'payload/errors' import { fieldAffectsData, optionIsObject } from 'payload/types' import toSnakeCase from 'to-snake-case' -import type { GenericColumns, PostgresAdapter } from '../types' +import type { GenericColumns, IDType, PostgresAdapter } from '../types' import { hasLocalesTable } from '../utilities/hasLocalesTable' import { buildTable } from './build' @@ -93,7 +94,8 @@ export const traverseFields = ({ let hasManyNumberField: 'index' | boolean = false let hasLocalizedManyNumberField = false - let parentIDColType = 'integer' + let parentIDColType: IDType = 'integer' + if (columns.id instanceof PgUUIDBuilder) parentIDColType = 'uuid' if (columns.id instanceof PgNumericBuilder) parentIDColType = 'numeric' if (columns.id instanceof PgVarcharBuilder) parentIDColType = 'varchar' diff --git a/packages/db-postgres/src/types.ts b/packages/db-postgres/src/types.ts index cfe5927193..e23c6e7345 100644 --- a/packages/db-postgres/src/types.ts +++ b/packages/db-postgres/src/types.ts @@ -16,6 +16,7 @@ import type { Pool, PoolConfig } from 'pg' export type DrizzleDB = NodePgDatabase> export type Args = { + idType?: 'serial' | 'uuid' logger?: DrizzleConfig['logger'] migrationDir?: string pool: PoolConfig @@ -56,6 +57,7 @@ export type PostgresAdapter = BaseDatabaseAdapter & { * Used for returning properly formed errors from unique fields */ fieldConstraints: Record> + idType: Args['idType'] logger: DrizzleConfig['logger'] pool: Pool poolOptions: Args['pool'] @@ -72,6 +74,8 @@ export type PostgresAdapter = BaseDatabaseAdapter & { tables: Record } +export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar' + export type PostgresAdapterResult = (args: { payload: Payload }) => PostgresAdapter export type MigrateUpArgs = { payload: Payload; req?: Partial } diff --git a/packages/db-postgres/src/upsertRow/insertArrays.ts b/packages/db-postgres/src/upsertRow/insertArrays.ts index 9193389f02..f187f4c002 100644 --- a/packages/db-postgres/src/upsertRow/insertArrays.ts +++ b/packages/db-postgres/src/upsertRow/insertArrays.ts @@ -36,7 +36,7 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P } } - const parentID = parentRows[parentRowIndex].id || parentRows[parentRowIndex]._parentID + const parentID = parentRows[parentRowIndex].id // Add any sub arrays that need to be created // We will call this recursively below @@ -61,8 +61,10 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P // Insert all corresponding arrays // (one insert per array table) for (const [tableName, row] of Object.entries(rowsByTable)) { + // the nested arrays need the ID for the parentID foreign key + let insertedRows: Args['parentRows'] if (row.rows.length > 0) { - await db.insert(adapter.tables[tableName]).values(row.rows).returning() + insertedRows = await db.insert(adapter.tables[tableName]).values(row.rows).returning() } // Insert locale rows @@ -76,7 +78,7 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P adapter, arrays: row.arrays, db, - parentRows: row.rows, + parentRows: insertedRows, }) } } diff --git a/test/buildConfigWithDefaults.ts b/test/buildConfigWithDefaults.ts index 355460e517..02ef05feb2 100644 --- a/test/buildConfigWithDefaults.ts +++ b/test/buildConfigWithDefaults.ts @@ -35,6 +35,13 @@ const databaseAdapters = { connectionString: process.env.POSTGRES_URL || 'postgres://127.0.0.1:5432/payloadtests', }, }), + 'postgres-uuid': postgresAdapter({ + idType: 'uuid', + migrationDir, + pool: { + connectionString: process.env.POSTGRES_URL || 'postgres://127.0.0.1:5432/payloadtests', + }, + }), supabase: postgresAdapter({ migrationDir, pool: {