fix!: handle custom id logic in mongodb adapter (#9069)

### What?
Moved the logic for copying the data.id to data._id to the mongoose
adapter.

### Why?
If you have any hooks that need to set the `id`, the value does not get
sent to mongodb as you would expect since it was copied before the
beforeValidate hooks.

### How?
Now data._id is assigned only in the mongodb adapter's `create`
function.

BREAKING CHANGES:
When using custom ID fields, if you have any collection hooks for
beforeValidate, beforeChange then `data._id` will no longer be assigned
as this happens now in the database adapter. Use `data.id` instead.
This commit is contained in:
Dan Ribbens
2024-11-08 15:34:19 -05:00
committed by GitHub
parent dc111041cb
commit ee117bb616
6 changed files with 296 additions and 13 deletions

View File

@@ -356,7 +356,7 @@ For full details on Admin Options, see the [Field Admin Options](../admin/fields
## Custom ID 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`: 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: [ fields: [
{ {
name: 'id', // highlight-line name: 'id', // highlight-line
required: true,
type: 'number', type: 'number',
}, },
], ],

View File

@@ -20,6 +20,10 @@ export const create: Create = async function create(
fields: this.payload.collections[collection].config.fields, fields: this.payload.collections[collection].config.fields,
}) })
if (this.payload.collections[collection].customIDType) {
sanitizedData._id = sanitizedData.id
}
try { try {
;[doc] = await Model.create([sanitizedData], options) ;[doc] = await Model.create([sanitizedData], options)
} catch (error) { } catch (error) {

View File

@@ -123,17 +123,6 @@ export const createOperation = async <
await executeAccess({ data, req }, collectionConfig.access.create) 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 // Generate data for all files and sizes
// ///////////////////////////////////// // /////////////////////////////////////

View File

@@ -4,6 +4,8 @@ const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
import type { TextField } from 'payload' import type { TextField } from 'payload'
import { v4 as uuid } from 'uuid'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.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: [ globals: [
{ {

View File

@@ -75,10 +75,19 @@ describe('database', () => {
expect(updated.id).toStrictEqual(created.doc.id) 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', () => { 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({ const result = await payload.create({
collection: 'posts', collection: 'posts',
data: { data: {

View File

@@ -19,21 +19,44 @@ export interface Config {
'custom-schema': CustomSchema; 'custom-schema': CustomSchema;
places: Place; places: Place;
'fields-persistance': FieldsPersistance; 'fields-persistance': FieldsPersistance;
'custom-ids': CustomId;
users: User; users: User;
'payload-locked-documents': PayloadLockedDocument; 'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference; 'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration; 'payload-migrations': PayloadMigration;
}; };
collectionsSelect: {
posts: PostsSelect<false> | PostsSelect<true>;
'default-values': DefaultValuesSelect<false> | DefaultValuesSelect<true>;
'relation-a': RelationASelect<false> | RelationASelect<true>;
'relation-b': RelationBSelect<false> | RelationBSelect<true>;
'pg-migrations': PgMigrationsSelect<false> | PgMigrationsSelect<true>;
'custom-schema': CustomSchemaSelect<false> | CustomSchemaSelect<true>;
places: PlacesSelect<false> | PlacesSelect<true>;
'fields-persistance': FieldsPersistanceSelect<false> | FieldsPersistanceSelect<true>;
'custom-ids': CustomIdsSelect<false> | CustomIdsSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: { db: {
defaultIDType: string; defaultIDType: string;
}; };
globals: { globals: {
global: Global; global: Global;
}; };
globalsSelect: {
global: GlobalSelect<false> | GlobalSelect<true>;
};
locale: 'en' | 'es'; locale: 'en' | 'es';
user: User & { user: User & {
collection: 'users'; collection: 'users';
}; };
jobs?: {
tasks: unknown;
workflows?: unknown;
};
} }
export interface UserAuthOperations { export interface UserAuthOperations {
forgotPassword: { forgotPassword: {
@@ -232,6 +255,15 @@ export interface FieldsPersistance {
updatedAt: string; updatedAt: string;
createdAt: 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;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users". * via the `definition` "users".
@@ -288,6 +320,10 @@ export interface PayloadLockedDocument {
relationTo: 'fields-persistance'; relationTo: 'fields-persistance';
value: string | FieldsPersistance; value: string | FieldsPersistance;
} | null) } | null)
| ({
relationTo: 'custom-ids';
value: string | CustomId;
} | null)
| ({ | ({
relationTo: 'users'; relationTo: 'users';
value: string | User; value: string | User;
@@ -334,6 +370,215 @@ export interface PayloadMigration {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts_select".
*/
export interface PostsSelect<T extends boolean = true> {
title?: T;
hasTransaction?: T;
throwAfterChange?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "default-values_select".
*/
export interface DefaultValuesSelect<T extends boolean = true> {
title?: T;
defaultValue?: T;
array?:
| T
| {
defaultValue?: T;
id?: T;
};
group?:
| T
| {
defaultValue?: T;
};
select?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "relation-a_select".
*/
export interface RelationASelect<T extends boolean = true> {
title?: T;
richText?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "relation-b_select".
*/
export interface RelationBSelect<T extends boolean = true> {
title?: T;
relationship?: T;
richText?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pg-migrations_select".
*/
export interface PgMigrationsSelect<T extends boolean = true> {
relation1?: T;
myArray?:
| T
| {
relation2?: T;
mySubArray?:
| T
| {
relation3?: T;
id?: T;
};
id?: T;
};
myGroup?:
| T
| {
relation4?: T;
};
myBlocks?:
| T
| {
myBlock?:
| T
| {
relation5?: T;
relation6?: T;
id?: T;
blockName?: T;
};
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "custom-schema_select".
*/
export interface CustomSchemaSelect<T extends boolean = true> {
text?: T;
localizedText?: T;
relationship?: T;
select?: T;
radio?: T;
array?:
| T
| {
text?: T;
localizedText?: T;
id?: T;
};
blocks?:
| T
| {
block?:
| T
| {
text?: T;
localizedText?: T;
id?: T;
blockName?: T;
};
};
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "places_select".
*/
export interface PlacesSelect<T extends boolean = true> {
country?: T;
city?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "fields-persistance_select".
*/
export interface FieldsPersistanceSelect<T extends boolean = true> {
text?: T;
textHooked?: T;
array?:
| T
| {
id?: T;
};
textWithinRow?: T;
textWithinCollapsible?: T;
textWithinTabs?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "custom-ids_select".
*/
export interface CustomIdsSelect<T extends boolean = true> {
id?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
loginAttempts?: T;
lockUntil?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "global". * via the `definition` "global".
@@ -344,6 +589,16 @@ export interface Global {
updatedAt?: string | null; updatedAt?: string | null;
createdAt?: string | null; createdAt?: string | null;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "global_select".
*/
export interface GlobalSelect<T extends boolean = true> {
text?: T;
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth". * via the `definition` "auth".