diff --git a/docs/fields/overview.mdx b/docs/fields/overview.mdx index fcd346505c..eed310084b 100644 --- a/docs/fields/overview.mdx +++ b/docs/fields/overview.mdx @@ -356,7 +356,7 @@ For full details on Admin Options, see the [Field Admin Options](../admin/fields ## Custom ID Fields -All [Collections](../configuration/collections) automatically generate their own ID field. If needed, you can override this behavior by providing an explicit ID field to your config. This will force users to provide a their own ID value when creating a record. +All [Collections](../configuration/collections) automatically generate their own ID field. If needed, you can override this behavior by providing an explicit ID field to your config. This field should either be required or have a hook to generate the ID dynamically. To define a custom ID field, add a new field with the `name` property set to `id`: @@ -368,6 +368,7 @@ export const MyCollection: CollectionConfig = { fields: [ { name: 'id', // highlight-line + required: true, type: 'number', }, ], diff --git a/packages/db-mongodb/src/create.ts b/packages/db-mongodb/src/create.ts index d1ac9a6829..504cffd854 100644 --- a/packages/db-mongodb/src/create.ts +++ b/packages/db-mongodb/src/create.ts @@ -20,6 +20,10 @@ export const create: Create = async function create( fields: this.payload.collections[collection].config.fields, }) + if (this.payload.collections[collection].customIDType) { + sanitizedData._id = sanitizedData.id + } + try { ;[doc] = await Model.create([sanitizedData], options) } catch (error) { diff --git a/packages/payload/src/collections/operations/create.ts b/packages/payload/src/collections/operations/create.ts index 574d3e2c6d..a5580200f0 100644 --- a/packages/payload/src/collections/operations/create.ts +++ b/packages/payload/src/collections/operations/create.ts @@ -123,17 +123,6 @@ export const createOperation = async < await executeAccess({ data, req }, collectionConfig.access.create) } - // ///////////////////////////////////// - // Custom id - // ///////////////////////////////////// - - if (payload.collections[collectionConfig.slug].customIDType) { - data = { - _id: data.id, - ...data, - } - } - // ///////////////////////////////////// // Generate data for all files and sizes // ///////////////////////////////////// diff --git a/packages/payload/src/database/migrations/migrateRefresh.ts b/packages/payload/src/database/migrations/migrateRefresh.ts index 82d90191db..0b53eca724 100644 --- a/packages/payload/src/database/migrations/migrateRefresh.ts +++ b/packages/payload/src/database/migrations/migrateRefresh.ts @@ -14,59 +14,57 @@ export async function migrateRefresh(this: BaseDatabaseAdapter) { const { payload } = this const migrationFiles = await readMigrationFiles({ payload }) - const { existingMigrations, latestBatch } = await getMigrations({ + const { existingMigrations } = await getMigrations({ payload, }) - if (!existingMigrations?.length) { - payload.logger.info({ msg: 'No migrations to rollback.' }) - return - } - - payload.logger.info({ - msg: `Rolling back batch ${latestBatch} consisting of ${existingMigrations.length} migration(s).`, - }) - const req = { payload } as PayloadRequest - // Reverse order of migrations to rollback - existingMigrations.reverse() + if (existingMigrations?.length) { + payload.logger.info({ + msg: `Rolling back all ${existingMigrations.length} migration(s).`, + }) + // Reverse order of migrations to rollback + existingMigrations.reverse() - for (const migration of existingMigrations) { - try { - const migrationFile = migrationFiles.find((m) => m.name === migration.name) - if (!migrationFile) { - throw new Error(`Migration ${migration.name} not found locally.`) - } + for (const migration of existingMigrations) { + try { + const migrationFile = migrationFiles.find((m) => m.name === migration.name) + if (!migrationFile) { + throw new Error(`Migration ${migration.name} not found locally.`) + } - payload.logger.info({ msg: `Migrating down: ${migration.name}` }) - const start = Date.now() - await initTransaction(req) - await migrationFile.down({ payload, req }) - payload.logger.info({ - msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`, - }) - await payload.delete({ - collection: 'payload-migrations', - req, - where: { - name: { - equals: migration.name, + payload.logger.info({ msg: `Migrating down: ${migration.name}` }) + const start = Date.now() + await initTransaction(req) + await migrationFile.down({ payload, req }) + payload.logger.info({ + msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`, + }) + await payload.delete({ + collection: 'payload-migrations', + req, + where: { + name: { + equals: migration.name, + }, }, - }, - }) - } catch (err: unknown) { - await killTransaction(req) - let msg = `Error running migration ${migration.name}. Rolling back.` - if (err instanceof Error) { - msg += ` ${err.message}` + }) + } catch (err: unknown) { + await killTransaction(req) + let msg = `Error running migration ${migration.name}. Rolling back.` + if (err instanceof Error) { + msg += ` ${err.message}` + } + payload.logger.error({ + err, + msg, + }) + process.exit(1) } - payload.logger.error({ - err, - msg, - }) - process.exit(1) } + } else { + payload.logger.info({ msg: 'No migrations to rollback.' }) } // Run all migrate up diff --git a/test/database/config.ts b/test/database/config.ts index 2d23bd29a0..641dd7b352 100644 --- a/test/database/config.ts +++ b/test/database/config.ts @@ -4,6 +4,8 @@ const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) import type { TextField } from 'payload' +import { v4 as uuid } from 'uuid' + import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import { devUser } from '../credentials.js' @@ -356,6 +358,29 @@ export default buildConfigWithDefaults({ }, ], }, + { + slug: 'custom-ids', + fields: [ + { + name: 'id', + type: 'text', + admin: { + readOnly: true, + }, + hooks: { + beforeChange: [ + ({ value, operation }) => { + if (operation === 'create') { + return uuid() + } + return value + }, + ], + }, + }, + ], + versions: { drafts: true }, + }, ], globals: [ { diff --git a/test/database/int.spec.ts b/test/database/int.spec.ts index 46a40ae762..ed39466349 100644 --- a/test/database/int.spec.ts +++ b/test/database/int.spec.ts @@ -13,6 +13,7 @@ import { fileURLToPath } from 'url' import { devUser } from '../credentials.js' import { initPayloadInt } from '../helpers/initPayloadInt.js' +import { isMongoose } from '../helpers/isMongoose.js' import removeFiles from '../helpers/removeFiles.js' const filename = fileURLToPath(import.meta.url) @@ -75,10 +76,19 @@ describe('database', () => { expect(updated.id).toStrictEqual(created.doc.id) }) + + it('should create with generated ID text from hook', async () => { + const doc = await payload.create({ + collection: 'custom-ids', + data: {}, + }) + + expect(doc.id).toBeDefined() + }) }) describe('timestamps', () => { - it('should have createdAt and updatedAt timetstamps to the millisecond', async () => { + it('should have createdAt and updatedAt timestamps to the millisecond', async () => { const result = await payload.create({ collection: 'posts', data: { @@ -125,8 +135,14 @@ describe('database', () => { }) describe('migrations', () => { + let ranFreshTest = false + beforeEach(async () => { - if (process.env.PAYLOAD_DROP_DATABASE === 'true' && 'drizzle' in payload.db) { + if ( + process.env.PAYLOAD_DROP_DATABASE === 'true' && + 'drizzle' in payload.db && + !ranFreshTest + ) { const db = payload.db as unknown as PostgresAdapter await db.dropDatabase({ adapter: db }) } @@ -186,28 +202,47 @@ describe('database', () => { const migration = docs[0] expect(migration.name).toContain('_test') expect(migration.batch).toStrictEqual(1) + ranFreshTest = true }) - // known issue: https://github.com/payloadcms/payload/issues/4597 - it.skip('should run migrate:down', async () => { + it('should run migrate:down', async () => { + // known drizzle issue: https://github.com/payloadcms/payload/issues/4597 + if (!isMongoose(payload)) { + return + } let error try { await payload.db.migrateDown() } catch (e) { error = e } + + const migrations = await payload.find({ + collection: 'payload-migrations', + }) + expect(error).toBeUndefined() + expect(migrations.docs).toHaveLength(0) }) - // known issue: https://github.com/payloadcms/payload/issues/4597 - it.skip('should run migrate:refresh', async () => { + it('should run migrate:refresh', async () => { + // known drizzle issue: https://github.com/payloadcms/payload/issues/4597 + if (!isMongoose(payload)) { + return + } let error try { await payload.db.migrateRefresh() } catch (e) { error = e } + + const migrations = await payload.find({ + collection: 'payload-migrations', + }) + expect(error).toBeUndefined() + expect(migrations.docs).toHaveLength(1) }) }) diff --git a/test/database/payload-types.ts b/test/database/payload-types.ts index d042a4f07b..05a3ea82b0 100644 --- a/test/database/payload-types.ts +++ b/test/database/payload-types.ts @@ -19,6 +19,7 @@ export interface Config { 'custom-schema': CustomSchema; places: Place; 'fields-persistance': FieldsPersistance; + 'custom-ids': CustomId; users: User; 'payload-locked-documents': PayloadLockedDocument; 'payload-preferences': PayloadPreference; @@ -34,6 +35,7 @@ export interface Config { 'custom-schema': CustomSchemaSelect | CustomSchemaSelect; places: PlacesSelect | PlacesSelect; 'fields-persistance': FieldsPersistanceSelect | FieldsPersistanceSelect; + 'custom-ids': CustomIdsSelect | CustomIdsSelect; users: UsersSelect | UsersSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; @@ -254,6 +256,16 @@ export interface FieldsPersistance { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "custom-ids". + */ +export interface CustomId { + id: string; + updatedAt: string; + createdAt: string; + _status?: ('draft' | 'published') | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "users". @@ -310,6 +322,10 @@ export interface PayloadLockedDocument { relationTo: 'fields-persistance'; value: string | FieldsPersistance; } | null) + | ({ + relationTo: 'custom-ids'; + value: string | CustomId; + } | null) | ({ relationTo: 'users'; value: string | User; @@ -509,6 +525,16 @@ export interface FieldsPersistanceSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "custom-ids_select". + */ +export interface CustomIdsSelect { + id?: T; + updatedAt?: T; + createdAt?: T; + _status?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "users_select".