feat(drizzle): add uuidv7 support (#16113)

## Goal

Let projects use **UUID v7** (time-ordered UUIDs) for collection IDs via
`idType: 'uuidv7'` on Postgres-related adapters (and SQLite wiring),
while keeping the same storage shape as v4 UUIDs (native `uuid` on
Postgres, text on SQLite).

### What?

- Adds a new adapter option `idType: 'uuidv7'` alongside existing
`'serial' | 'uuid'` (Postgres) and `'number' | 'uuid'` (SQLite).
- Generates IDs in application code with `uuid` package `v7()` (not
database-native), so older Postgres versions are still supported.
- Reuses the same Drizzle column modeling as UUID v4 where appropriate
(`defaultV7` on the raw column, `$defaultFn` / generated schema code).
- Adds integration tests behind `PAYLOAD_DATABASE=postgres-uuidv7` and a
test adapter preset `postgres-uuidv7`.

### Why?

UUID v7 is sortable by creation time and tends to be friendlier for
B-tree indexes than random UUID v4, without changing the wire format or
Postgres column type.

### How?

- Bump `uuid` in `@payloadcms/drizzle` to a version that exports `v7`.
- Extend TypeScript `idType` unions and `UUIDRawColumn` with
`defaultV7`.
- In schema builders (`setColumnID`, `buildDrizzleTable`,
`columnToCodeConverter`), set app-side defaults for v7; treat `uuidv7`
like `uuid` for relationships and query sanitization via a small
`isUUIDType` helper and `getCollectionIdType` mapping to `'text'`.



Fixes #11449

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1213991890379567

---------

Co-authored-by: Sasha Rakhmatulin <sasha@ritsuko.dev>
This commit is contained in:
W1ckh3ad
2026-04-09 14:32:01 +01:00
committed by GitHub
co-authored by Sasha Rakhmatulin
parent 79f4cc7fae
commit ac01e82bcd
44 changed files with 667 additions and 476 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ description: Starts the required database for tests
inputs:
database:
description: 'Database type: mongodb, mongodb-atlas, cosmosdb, documentdb, firestore, postgres, postgres-custom-schema, postgres-uuid, postgres-read-replica, postgres-read-replicas, vercel-postgres-read-replica, sqlite, sqlite-uuid, supabase, d1'
description: 'Database type: mongodb, mongodb-atlas, cosmosdb, documentdb, firestore, postgres, postgres-custom-schema, postgres-uuid, postgres-uuidv7, postgres-read-replica, postgres-read-replicas, vercel-postgres-read-replica, sqlite, sqlite-uuid, sqlite-uuidv7, supabase, d1'
required: true
outputs:
+2
View File
@@ -10,10 +10,12 @@ createIntConfig({
'postgres',
'postgres-custom-schema',
'postgres-uuid',
'postgres-uuidv7',
'postgres-read-replica',
'supabase',
'sqlite',
'sqlite-uuid',
'sqlite-uuidv7',
],
shards: 3,
})
+2
View File
@@ -134,9 +134,11 @@ Set `PAYLOAD_DATABASE` in your `.env` file to choose the database adapter:
- `postgres` - PostgreSQL with pgvector and PostGIS
- `postgres-custom-schema` - PostgreSQL with custom schema
- `postgres-uuid` - PostgreSQL with UUID primary keys
- `postgres-uuidv7` - PostgreSQL with UUID V7 primary keys
- `postgres-read-replica` - PostgreSQL with read replica
- `sqlite` - SQLite
- `sqlite-uuid` - SQLite with UUID primary keys
- `sqlite-uuidv7` - SQLite with UUID V7 primary keys
- `supabase` - Supabase (PostgreSQL)
- `d1` - D1 (SQLite)
+1 -2
View File
@@ -77,13 +77,12 @@
"drizzle-orm": "0.44.7",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",
"uuid": "9.0.0"
"uuid": "11.1.0"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/pg": "8.10.2",
"@types/to-snake-case": "1.0.0",
"@types/uuid": "10.0.0",
"payload": "workspace:*"
},
"peerDependencies": {
+1 -1
View File
@@ -64,7 +64,7 @@ const filename = fileURLToPath(import.meta.url)
export function sqliteD1Adapter(args: Args): DatabaseAdapterObj<SQLiteD1Adapter> {
const sqliteIDType = args.idType || 'number'
const payloadIDType = sqliteIDType === 'uuid' ? 'text' : 'number'
const payloadIDType = sqliteIDType === 'uuid' || sqliteIDType === 'uuidv7' ? 'text' : 'number'
const allowIDOnCreate = args.allowIDOnCreate ?? false
function adapter({ payload }: { payload: Payload }) {
+1 -2
View File
@@ -55,13 +55,12 @@
"mongoose": "8.15.1",
"mongoose-paginate-v2": "1.9.4",
"prompts": "2.4.2",
"uuid": "10.0.0"
"uuid": "11.1.0"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/mongoose-aggregate-paginate-v2": "1.0.12",
"@types/prompts": "^2.4.5",
"@types/uuid": "10.0.0",
"mongodb": "6.16.0",
"mongodb-memory-server": "10.1.4",
"payload": "workspace:*"
+1 -1
View File
@@ -82,7 +82,7 @@
"pg": "8.16.3",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",
"uuid": "10.0.0"
"uuid": "11.1.0"
},
"devDependencies": {
"@hyrious/esbuild-plugin-commonjs": "0.2.6",
+1 -1
View File
@@ -53,7 +53,7 @@ export type Args = {
extensions?: string[]
/** Generated schema from payload generate:db-schema file path */
generateSchemaOutputFile?: string
idType?: 'serial' | 'uuid'
idType?: 'serial' | 'uuid' | 'uuidv7'
localesSuffix?: string
logger?: DrizzleConfig['logger']
migrationDir?: string
+1 -2
View File
@@ -79,13 +79,12 @@
"drizzle-orm": "0.44.7",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",
"uuid": "9.0.0"
"uuid": "11.1.0"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/pg": "8.10.2",
"@types/to-snake-case": "1.0.0",
"@types/uuid": "10.0.0",
"payload": "workspace:*"
},
"peerDependencies": {
+1 -1
View File
@@ -66,7 +66,7 @@ const filename = fileURLToPath(import.meta.url)
export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
const sqliteIDType = args.idType || 'number'
const payloadIDType = sqliteIDType === 'uuid' ? 'text' : 'number'
const payloadIDType = sqliteIDType === 'uuid' || sqliteIDType === 'uuidv7' ? 'text' : 'number'
const allowIDOnCreate = args.allowIDOnCreate ?? false
function adapter({ payload }: { payload: Payload }) {
+1 -1
View File
@@ -82,7 +82,7 @@
"pg": "8.16.3",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",
"uuid": "10.0.0"
"uuid": "11.1.0"
},
"devDependencies": {
"@hyrious/esbuild-plugin-commonjs": "0.2.6",
+1 -1
View File
@@ -53,7 +53,7 @@ export type Args = {
forceUseVercelPostgres?: boolean
/** Generated schema from payload generate:db-schema file path */
generateSchemaOutputFile?: string
idType?: 'serial' | 'uuid'
idType?: 'serial' | 'uuid' | 'uuidv7'
localesSuffix?: string
logger?: DrizzleConfig['logger']
migrationDir?: string
+1 -1
View File
@@ -62,7 +62,7 @@
"drizzle-orm": "0.44.7",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",
"uuid": "9.0.0"
"uuid": "11.1.0"
},
"devDependencies": {
"@libsql/client": "0.14.0",
@@ -68,6 +68,11 @@ export const columnToCodeConverter: ColumnToCodeConverter = ({
code = `${code}.defaultRandom()`
}
if (column.type === 'uuid' && column.defaultV7) {
addImport('uuid', 'v7 as uuidv7')
code = `${code}.$defaultFn(() => uuidv7())`
}
if (column.notNull) {
code = `${code}.notNull()`
}
@@ -18,6 +18,7 @@ import {
varchar,
vector,
} from 'drizzle-orm/pg-core'
import { v7 as uuidv7 } from 'uuid'
import type { RawColumn, RawTable } from '../../types.js'
import type { BasePostgresAdapter } from '../types.js'
@@ -109,6 +110,10 @@ export const buildDrizzleTable = ({
builder = builder.defaultRandom()
}
if (column.defaultV7) {
builder = builder.$defaultFn(() => uuidv7())
}
columns[key] = builder
break
}
@@ -34,6 +34,17 @@ export const setColumnID: SetColumnID = ({ adapter, columns, fields }) => {
return 'uuid'
}
if (adapter.idType === 'uuidv7') {
columns.id = {
name: 'id',
type: 'uuid',
defaultV7: true,
primaryKey: true,
}
return 'uuid'
}
columns.id = {
name: 'id',
type: 'serial',
+1 -1
View File
@@ -147,7 +147,7 @@ export type BasePostgresAdapter = {
* Used for returning properly formed errors from unique fields
*/
fieldConstraints: Record<string, Record<string, string>>
idType: 'serial' | 'uuid'
idType: 'serial' | 'uuid' | 'uuidv7'
initializing: Promise<void>
insert: Insert
localesSuffix?: string
@@ -19,6 +19,7 @@ import type { DrizzleAdapter, GenericColumn } from '../types.js'
import type { BuildQueryJoinAliases } from './buildQuery.js'
import { isPolymorphicRelationship } from '../utilities/isPolymorphicRelationship.js'
import { isUUIDType } from '../utilities/isUUIDType.js'
import { jsonBuildObject } from '../utilities/json.js'
import { DistinctSymbol } from '../utilities/rawConstraint.js'
import { resolveBlockTableName } from '../utilities/validateExistingBlockIsIdentical.js'
@@ -111,7 +112,7 @@ export const getTableColumnFromPath = ({
constraints,
field: {
name: 'id',
type: adapter.idType === 'uuid' ? 'text' : 'number',
type: isUUIDType(adapter.idType) ? 'text' : 'number',
} as NumberField | TextField,
table: adapter.tables[newTableName],
}
@@ -414,7 +415,7 @@ export const getTableColumnFromPath = ({
constraints,
field: {
name: 'id',
type: adapter.idType === 'uuid' ? 'text' : 'number',
type: isUUIDType(adapter.idType) ? 'text' : 'number',
} as NumberField | TextField,
table: aliasRelationshipTable,
}
@@ -531,7 +532,7 @@ export const getTableColumnFromPath = ({
constraints,
field: {
name: 'id',
type: adapter.idType === 'uuid' ? 'text' : 'number',
type: isUUIDType(adapter.idType) ? 'text' : 'number',
} as NumberField | TextField,
table: newAliasTable,
}
@@ -706,8 +707,9 @@ export const getTableColumnFromPath = ({
const columns: TableColumn['columns'] = field.relationTo
.map((relationTo) => {
let idType: 'number' | 'text' | 'uuid' =
adapter.idType === 'uuid' ? 'uuid' : 'number'
let idType: 'number' | 'text' | 'uuid' = isUUIDType(adapter.idType)
? 'uuid'
: 'number'
const { customIDType } = adapter.payload.collections[relationTo]
@@ -8,6 +8,7 @@ import type { DrizzleAdapter } from '../types.js'
import { getCollectionIdType } from '../utilities/getCollectionIdType.js'
import { isPolymorphicRelationship } from '../utilities/isPolymorphicRelationship.js'
import { isUUIDType } from '../utilities/isUUIDType.js'
import { isRawConstraint } from '../utilities/rawConstraint.js'
type SanitizeQueryValueArgs = {
@@ -59,7 +60,7 @@ export const sanitizeQueryValue = ({
) {
const allPossibleIDTypes: (number | string)[] = []
formattedValue.forEach((val) => {
if (adapter.idType !== 'uuid' && typeof val === 'string') {
if (!isUUIDType(adapter.idType) && typeof val === 'string') {
allPossibleIDTypes.push(val, parseInt(val))
} else if (typeof val === 'string') {
allPossibleIDTypes.push(val)
+4 -2
View File
@@ -18,6 +18,7 @@ import type {
import { createTableName } from '../createTableName.js'
import { buildForeignKeyName } from '../utilities/buildForeignKeyName.js'
import { buildIndexName } from '../utilities/buildIndexName.js'
import { isUUIDType } from '../utilities/isUUIDType.js'
import { traverseFields } from './traverseFields.js'
type Args = {
@@ -619,8 +620,9 @@ export const buildTable = ({
config: relationshipConfig,
throwValidationError: true,
})
let colType: 'integer' | 'numeric' | 'uuid' | 'varchar' =
adapter.idType === 'uuid' ? 'uuid' : 'integer'
let colType: 'integer' | 'numeric' | 'uuid' | 'varchar' = isUUIDType(adapter.idType)
? 'uuid'
: 'integer'
const relatedCollectionCustomIDType =
adapter.payload.collections[relationshipConfig.slug]?.customIDType
@@ -24,6 +24,7 @@ import { createTableName } from '../createTableName.js'
import { buildIndexName } from '../utilities/buildIndexName.js'
import { getArrayRelationName } from '../utilities/getArrayRelationName.js'
import { hasLocalesTable } from '../utilities/hasLocalesTable.js'
import { isUUIDType } from '../utilities/isUUIDType.js'
import {
InternalBlockTableNameIndex,
setInternalBlockIndex,
@@ -945,7 +946,7 @@ export const traverseFields = ({
const tableName = adapter.tableNameMap.get(toSnakeCase(field.relationTo))
// get the id type of the related collection
let colType: IDType = adapter.idType === 'uuid' ? 'uuid' : 'integer'
let colType: IDType = isUUIDType(adapter.idType) ? 'uuid' : 'integer'
const relatedCollectionCustomID = relationshipConfig.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
@@ -69,6 +69,11 @@ export const columnToCodeConverter: ColumnToCodeConverter = ({
defaultStatement = `$defaultFn(() => randomUUID())`
}
if (column.defaultV7) {
addImport('uuid', 'v7 as uuidv7')
defaultStatement = `$defaultFn(() => uuidv7())`
}
break
}
@@ -10,7 +10,7 @@ import {
text,
uniqueIndex,
} from 'drizzle-orm/sqlite-core'
import { v4 as uuidv4 } from 'uuid'
import { v4 as uuidv4, v7 as uuidv7 } from 'uuid'
import type { BuildDrizzleTable, RawColumn } from '../../types.js'
@@ -72,6 +72,10 @@ export const buildDrizzleTable: BuildDrizzleTable = ({ adapter, locales, rawTabl
builder = builder.$defaultFn(() => uuidv4())
}
if (column.defaultV7) {
builder = builder.$defaultFn(() => uuidv7())
}
columns[key] = builder
break
}
@@ -34,6 +34,17 @@ export const setColumnID: SetColumnID = ({ adapter, columns, fields }) => {
return 'uuid'
}
if (adapter.idType === 'uuidv7') {
columns.id = {
name: 'id',
type: 'uuid',
defaultV7: true,
primaryKey: true,
}
return 'uuid'
}
columns.id = {
name: 'id',
type: 'integer',
+1 -1
View File
@@ -60,7 +60,7 @@ export type BaseSQLiteArgs = {
blocksAsJSON?: boolean
/** Generated schema from payload generate:db-schema file path */
generateSchemaOutputFile?: string
idType?: 'number' | 'uuid'
idType?: 'number' | 'uuid' | 'uuidv7'
localesSuffix?: string
logger?: DrizzleConfig['logger']
migrationDir?: string
+3 -1
View File
@@ -248,6 +248,8 @@ export type TimestampRawColumn = {
*/
export type UUIDRawColumn = {
defaultRandom?: boolean
/** App-side UUID v7 default (Postgres & SQLite); mutually exclusive with defaultRandom in practice */
defaultV7?: boolean
type: 'uuid'
} & BaseRawColumn
@@ -404,7 +406,7 @@ export interface DrizzleAdapter extends BaseDatabaseAdapter {
fieldConstraints: Record<string, Record<string, string>>
foreignKeys: Set<string>
idType: 'serial' | 'uuid'
idType: 'serial' | 'uuid' | 'uuidv7'
indexes: Set<string>
initializing: Promise<void>
insert: Insert
@@ -7,6 +7,7 @@ const typeMap: Record<string, 'number' | 'text'> = {
serial: 'number',
text: 'text',
uuid: 'text',
uuidv7: 'text',
}
export const getCollectionIdType = ({
@@ -0,0 +1 @@
export const isUUIDType = (idType: string): boolean => idType === 'uuid' || idType === 'uuidv7'
+1 -2
View File
@@ -123,7 +123,7 @@
"path-to-regexp": "6.3.0",
"qs-esm": "8.0.1",
"sass": "1.77.4",
"uuid": "10.0.0"
"uuid": "11.1.0"
},
"devDependencies": {
"@babel/cli": "7.27.2",
@@ -136,7 +136,6 @@
"@types/busboy": "1.5.4",
"@types/react": "19.2.9",
"@types/react-dom": "19.2.3",
"@types/uuid": "10.0.0",
"babel-plugin-react-compiler": "19.1.0-rc.3",
"esbuild": "0.27.1",
"esbuild-sass-plugin": "3.3.1",
+1 -2
View File
@@ -128,7 +128,7 @@
"ts-essentials": "10.0.3",
"tsx": "4.21.0",
"undici": "7.24.4",
"uuid": "10.0.0",
"uuid": "11.1.0",
"ws": "^8.16.0"
},
"devDependencies": {
@@ -140,7 +140,6 @@
"@types/nodemailer": "7.0.2",
"@types/pluralize": "0.0.33",
"@types/range-parser": "1.2.7",
"@types/uuid": "10.0.0",
"@types/ws": "^8.5.10",
"copyfiles": "2.4.1",
"cross-env": "7.0.3",
+1 -2
View File
@@ -66,7 +66,7 @@
"@payloadcms/ui": "workspace:*",
"lodash.get": "^4.4.2",
"stripe": "^10.2.0",
"uuid": "10.0.0"
"uuid": "11.1.0"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
@@ -74,7 +74,6 @@
"@types/lodash.get": "^4.4.7",
"@types/react": "19.2.9",
"@types/react-dom": "19.2.3",
"@types/uuid": "10.0.0",
"payload": "workspace:*"
},
"peerDependencies": {
+1 -2
View File
@@ -380,7 +380,6 @@
"@lexical/utils": "0.41.0",
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"@types/uuid": "10.0.0",
"acorn": "8.16.0",
"bson-objectid": "2.0.4",
"csstype": "3.1.3",
@@ -394,7 +393,7 @@
"qs-esm": "8.0.1",
"react-error-boundary": "4.1.2",
"ts-essentials": "10.0.3",
"uuid": "10.0.0"
"uuid": "11.1.0"
},
"devDependencies": {
"@babel/cli": "7.27.2",
+1 -2
View File
@@ -159,7 +159,7 @@
"sonner": "^1.7.2",
"ts-essentials": "10.0.3",
"use-context-selector": "2.0.0",
"uuid": "10.0.0"
"uuid": "11.1.0"
},
"devDependencies": {
"@babel/cli": "7.27.2",
@@ -171,7 +171,6 @@
"@payloadcms/eslint-config": "workspace:*",
"@types/react": "19.2.9",
"@types/react-dom": "19.2.3",
"@types/uuid": "10.0.0",
"babel-plugin-react-compiler": "19.1.0-rc.3",
"esbuild": "0.27.1",
"esbuild-sass-plugin": "3.3.1",
+57 -278
View File
@@ -67,10 +67,9 @@ export interface Config {
};
blocks: {};
collections: {
relation: Relation;
'posts-versioned': PostsVersioned;
'posts-batches': PostsBatch;
posts: Post;
categories: Category;
articles: Article;
'payload-kv': PayloadKv;
users: User;
'payload-locked-documents': PayloadLockedDocument;
@@ -79,10 +78,9 @@ export interface Config {
};
collectionsJoins: {};
collectionsSelect: {
relation: RelationSelect<false> | RelationSelect<true>;
'posts-versioned': PostsVersionedSelect<false> | PostsVersionedSelect<true>;
'posts-batches': PostsBatchesSelect<false> | PostsBatchesSelect<true>;
posts: PostsSelect<false> | PostsSelect<true>;
categories: CategoriesSelect<false> | CategoriesSelect<true>;
articles: ArticlesSelect<false> | ArticlesSelect<true>;
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
@@ -90,18 +88,12 @@ export interface Config {
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: number;
defaultIDType: string;
};
fallbackLocale: ('false' | 'none' | 'null') | false | null | ('en' | 'fr') | ('en' | 'fr')[];
globals: {
'global-versioned': GlobalVersioned;
global: Global;
};
globalsSelect: {
'global-versioned': GlobalVersionedSelect<false> | GlobalVersionedSelect<true>;
global: GlobalSelect<false> | GlobalSelect<true>;
};
locale: 'en' | 'fr';
fallbackLocale: null;
globals: {};
globalsSelect: {};
locale: null;
widgets: {
collections: CollectionsWidget;
};
@@ -131,93 +123,32 @@ export interface UserAuthOperations {
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "relation".
* via the `definition` "posts".
*/
export interface Relation {
id: number;
export interface Post {
id: string;
title?: string | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "categories".
*/
export interface Category {
id: string;
name?: string | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts-versioned".
* via the `definition` "articles".
*/
export interface PostsVersioned {
id: number;
export interface Article {
id: string;
title?: string | null;
content?:
| {
text?: string | null;
relation?: (number | null) | Relation;
manyRelations?: (number | Relation)[] | null;
id?: string | null;
blockName?: string | null;
blockType: 'textBlock';
}[]
| null;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts-batches".
*/
export interface PostsBatch {
id: number;
title?: string | null;
content?:
| {
text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'textBlock';
}[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts".
*/
export interface Post {
id: number;
title?: string | null;
content?:
| (
| {
text?: string | null;
select?: ('option1' | 'option2') | null;
relation?: (number | null) | Relation;
manyRelations?: (number | Relation)[] | null;
id?: string | null;
blockName?: string | null;
blockType: 'textBlock';
}
| {
text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'block_second';
}
| {
text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'block_third';
}
)[]
| null;
localizedContent?:
| {
text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'textBlockLocalized';
}[]
| null;
category?: (string | null) | Category;
updatedAt: string;
createdAt: string;
}
@@ -226,7 +157,7 @@ export interface Post {
* via the `definition` "payload-kv".
*/
export interface PayloadKv {
id: number;
id: string;
key: string;
data:
| {
@@ -243,7 +174,7 @@ export interface PayloadKv {
* via the `definition` "users".
*/
export interface User {
id: number;
id: string;
updatedAt: string;
createdAt: string;
email: string;
@@ -268,32 +199,28 @@ export interface User {
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: number;
id: string;
document?:
| ({
relationTo: 'relation';
value: number | Relation;
} | null)
| ({
relationTo: 'posts-versioned';
value: number | PostsVersioned;
} | null)
| ({
relationTo: 'posts-batches';
value: number | PostsBatch;
} | null)
| ({
relationTo: 'posts';
value: number | Post;
value: string | Post;
} | null)
| ({
relationTo: 'categories';
value: string | Category;
} | null)
| ({
relationTo: 'articles';
value: string | Article;
} | null)
| ({
relationTo: 'users';
value: number | User;
value: string | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: number | User;
value: string | User;
};
updatedAt: string;
createdAt: string;
@@ -303,10 +230,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: number;
id: string;
user: {
relationTo: 'users';
value: number | User;
value: string | User;
};
key?: string | null;
value?:
@@ -326,7 +253,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: number;
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
@@ -334,101 +261,29 @@ export interface PayloadMigration {
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "relation_select".
* via the `definition` "posts_select".
*/
export interface RelationSelect<T extends boolean = true> {
export interface PostsSelect<T extends boolean = true> {
title?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "categories_select".
*/
export interface CategoriesSelect<T extends boolean = true> {
name?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts-versioned_select".
* via the `definition` "articles_select".
*/
export interface PostsVersionedSelect<T extends boolean = true> {
export interface ArticlesSelect<T extends boolean = true> {
title?: T;
content?:
| T
| {
textBlock?:
| T
| {
text?: T;
relation?: T;
manyRelations?: T;
id?: T;
blockName?: T;
};
};
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts-batches_select".
*/
export interface PostsBatchesSelect<T extends boolean = true> {
title?: T;
content?:
| T
| {
textBlock?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts_select".
*/
export interface PostsSelect<T extends boolean = true> {
title?: T;
content?:
| T
| {
textBlock?:
| T
| {
text?: T;
select?: T;
relation?: T;
manyRelations?: T;
id?: T;
blockName?: T;
};
block_second?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
block_third?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
};
localizedContent?:
| T
| {
textBlockLocalized?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
};
category?: T;
updatedAt?: T;
createdAt?: T;
}
@@ -494,82 +349,6 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "global-versioned".
*/
export interface GlobalVersioned {
id: number;
content?:
| {
text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'textBlock';
}[]
| null;
_status?: ('draft' | 'published') | null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "global".
*/
export interface Global {
id: number;
content?:
| {
text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'textBlock';
}[]
| null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "global-versioned_select".
*/
export interface GlobalVersionedSelect<T extends boolean = true> {
content?:
| T
| {
textBlock?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
};
_status?: T;
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "global_select".
*/
export interface GlobalSelect<T extends boolean = true> {
content?:
| T
| {
textBlock?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "collections_widget".
+33 -62
View File
@@ -322,8 +322,8 @@ importers:
specifier: 1.0.0
version: 1.0.0
uuid:
specifier: 9.0.0
version: 9.0.0
specifier: 11.1.0
version: 11.1.0
devDependencies:
'@payloadcms/eslint-config':
specifier: workspace:*
@@ -334,9 +334,6 @@ importers:
'@types/to-snake-case':
specifier: 1.0.0
version: 1.0.0
'@types/uuid':
specifier: 10.0.0
version: 10.0.0
payload:
specifier: workspace:*
version: link:../payload
@@ -353,8 +350,8 @@ importers:
specifier: 2.4.2
version: 2.4.2
uuid:
specifier: 10.0.0
version: 10.0.0
specifier: 11.1.0
version: 11.1.0
devDependencies:
'@payloadcms/eslint-config':
specifier: workspace:*
@@ -365,9 +362,6 @@ importers:
'@types/prompts':
specifier: ^2.4.5
version: 2.4.9
'@types/uuid':
specifier: 10.0.0
version: 10.0.0
mongodb:
specifier: 6.16.0
version: 6.16.0(@aws-sdk/credential-providers@3.1026.0)(socks@2.8.7)
@@ -405,8 +399,8 @@ importers:
specifier: 1.0.0
version: 1.0.0
uuid:
specifier: 10.0.0
version: 10.0.0
specifier: 11.1.0
version: 11.1.0
devDependencies:
'@hyrious/esbuild-plugin-commonjs':
specifier: 0.2.6
@@ -448,8 +442,8 @@ importers:
specifier: 1.0.0
version: 1.0.0
uuid:
specifier: 9.0.0
version: 9.0.0
specifier: 11.1.0
version: 11.1.0
devDependencies:
'@payloadcms/eslint-config':
specifier: workspace:*
@@ -460,9 +454,6 @@ importers:
'@types/to-snake-case':
specifier: 1.0.0
version: 1.0.0
'@types/uuid':
specifier: 10.0.0
version: 10.0.0
payload:
specifier: workspace:*
version: link:../payload
@@ -494,8 +485,8 @@ importers:
specifier: 1.0.0
version: 1.0.0
uuid:
specifier: 10.0.0
version: 10.0.0
specifier: 11.1.0
version: 11.1.0
devDependencies:
'@hyrious/esbuild-plugin-commonjs':
specifier: 0.2.6
@@ -534,8 +525,8 @@ importers:
specifier: 1.0.0
version: 1.0.0
uuid:
specifier: 9.0.0
version: 9.0.0
specifier: 11.1.0
version: 11.1.0
devDependencies:
'@libsql/client':
specifier: 0.14.0
@@ -819,8 +810,8 @@ importers:
specifier: 1.77.4
version: 1.77.4
uuid:
specifier: 10.0.0
version: 10.0.0
specifier: 11.1.0
version: 11.1.0
devDependencies:
'@babel/cli':
specifier: 7.27.2
@@ -852,9 +843,6 @@ importers:
'@types/react-dom':
specifier: 19.2.3
version: 19.2.3(@types/react@19.2.9)
'@types/uuid':
specifier: 10.0.0
version: 10.0.0
babel-plugin-react-compiler:
specifier: 19.1.0-rc.3
version: 19.1.0-rc.3
@@ -964,8 +952,8 @@ importers:
specifier: 7.24.4
version: 7.24.4
uuid:
specifier: 10.0.0
version: 10.0.0
specifier: 11.1.0
version: 11.1.0
ws:
specifier: ^8.16.0
version: 8.20.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
@@ -994,9 +982,6 @@ importers:
'@types/range-parser':
specifier: 1.2.7
version: 1.2.7
'@types/uuid':
specifier: 10.0.0
version: 10.0.0
'@types/ws':
specifier: ^8.5.10
version: 8.18.1
@@ -1359,8 +1344,8 @@ importers:
specifier: ^10.2.0
version: 10.17.0
uuid:
specifier: 10.0.0
version: 10.0.0
specifier: 11.1.0
version: 11.1.0
devDependencies:
'@payloadcms/eslint-config':
specifier: workspace:*
@@ -1377,9 +1362,6 @@ importers:
'@types/react-dom':
specifier: 19.2.3
version: 19.2.3(@types/react@19.2.9)
'@types/uuid':
specifier: 10.0.0
version: 10.0.0
payload:
specifier: workspace:*
version: link:../payload
@@ -1434,9 +1416,6 @@ importers:
'@payloadcms/ui':
specifier: workspace:*
version: link:../ui
'@types/uuid':
specifier: 10.0.0
version: 10.0.0
acorn:
specifier: 8.16.0
version: 8.16.0
@@ -1483,8 +1462,8 @@ importers:
specifier: 10.0.3
version: 10.0.3(typescript@5.7.3)
uuid:
specifier: 10.0.0
version: 10.0.0
specifier: 11.1.0
version: 11.1.0
devDependencies:
'@babel/cli':
specifier: 7.27.2
@@ -1805,8 +1784,8 @@ importers:
specifier: 2.0.0
version: 2.0.0(react@19.2.4)(scheduler@0.25.0)
uuid:
specifier: 10.0.0
version: 10.0.0
specifier: 11.1.0
version: 11.1.0
devDependencies:
'@babel/cli':
specifier: 7.27.2
@@ -1835,9 +1814,6 @@ importers:
'@types/react-dom':
specifier: 19.2.3
version: 19.2.3(@types/react@19.2.9)
'@types/uuid':
specifier: 10.0.0
version: 10.0.0
babel-plugin-react-compiler:
specifier: 19.1.0-rc.3
version: 19.1.0-rc.3
@@ -2581,8 +2557,8 @@ importers:
specifier: 5.7.3
version: 5.7.3
uuid:
specifier: 10.0.0
version: 10.0.0
specifier: 11.1.0
version: 11.1.0
vitest:
specifier: 4.1.2
version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@22.19.9)(@vitest/ui@4.1.2)(happy-dom@20.8.9(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@28.0.0(@noble/hashes@1.8.0))(vite@7.3.2(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.99.0)(sass@1.77.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
@@ -8080,9 +8056,6 @@ packages:
'@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
'@types/uuid@10.0.0':
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
'@types/webidl-conversions@7.0.3':
resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==}
@@ -14287,12 +14260,12 @@ packages:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
uuid@11.1.0:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
uuid@9.0.0:
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
uuid@9.0.1:
@@ -20232,7 +20205,7 @@ snapshots:
dependencies:
'@sentry/bundler-plugin-core': 2.22.7
unplugin: 1.0.1
uuid: 9.0.0
uuid: 9.0.1
webpack: 5.105.4(@swc/core@1.15.3)(esbuild@0.25.12)
transitivePeerDependencies:
- encoding
@@ -20242,7 +20215,7 @@ snapshots:
dependencies:
'@sentry/bundler-plugin-core': 3.6.1
unplugin: 1.0.1
uuid: 9.0.0
uuid: 9.0.1
webpack: 5.105.4(@swc/core@1.15.3)(esbuild@0.25.12)
transitivePeerDependencies:
- encoding
@@ -21413,8 +21386,6 @@ snapshots:
'@types/use-sync-external-store@0.0.6': {}
'@types/uuid@10.0.0': {}
'@types/webidl-conversions@7.0.3': {}
'@types/whatwg-mimetype@3.0.2': {}
@@ -28303,7 +28274,7 @@ snapshots:
https-proxy-agent: 5.0.1
node-fetch: 2.7.0
stream-events: 1.0.5
uuid: 9.0.0
uuid: 9.0.1
transitivePeerDependencies:
- encoding
- supports-color
@@ -28761,9 +28732,9 @@ snapshots:
uuid@10.0.0: {}
uuid@8.3.2: {}
uuid@11.1.0: {}
uuid@9.0.0: {}
uuid@8.3.2: {}
uuid@9.0.1: {}
+13
View File
@@ -96,6 +96,9 @@ export interface Config {
menu: MenuSelect<false> | MenuSelect<true>;
};
locale: null;
widgets: {
collections: CollectionsWidget;
};
user: User;
jobs: {
tasks: unknown;
@@ -435,6 +438,16 @@ export interface MenuSelect<T extends boolean = true> {
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "collections_widget".
*/
export interface CollectionsWidget {
data?: {
[k: string]: unknown;
};
width: 'full';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
+4 -2
View File
@@ -24,7 +24,9 @@ describe('Custom GraphQL', () => {
})
if (
!['cosmosdb', 'firestore', 'sqlite', 'sqlite-uuid'].includes(process.env.PAYLOAD_DATABASE || '')
!['cosmosdb', 'firestore', 'sqlite', 'sqlite-uuid', 'sqlite-uuidv7'].includes(
process.env.PAYLOAD_DATABASE || '',
)
) {
describe('Isolated Transaction ID', () => {
it('should isolate transaction IDs between queries in the same request', async () => {
@@ -61,7 +63,7 @@ describe('Custom GraphQL', () => {
})
})
} else {
it('should not run isolated transaction ID tests for sqlite/firestore/cosmosdb', () => {
it('should not run isolated transaction ID tests for sqlite (incl. uuid variants)/firestore/cosmosdb', () => {
expect(true).toBe(true)
})
}
+159 -5
View File
@@ -29,7 +29,7 @@ import {
} from 'payload'
import { assert } from 'ts-essentials'
import { fileURLToPath } from 'url'
import { afterAll, beforeAll, beforeEach, expect } from 'vitest'
import { afterAll, afterEach, beforeAll, beforeEach, expect } from 'vitest'
import type { NextRESTClient } from '../__helpers/shared/NextRESTClient.js'
import type { Global2, Post } from './payload-types.js'
@@ -41,10 +41,13 @@ import { removeFiles } from '../__helpers/shared/removeFiles.js'
import { devUser } from '../credentials.js'
import { seed } from './seed.js'
import {
customIDsSlug,
defaultValuesSlug,
errorOnUnnamedFieldsSlug,
fieldsPersistanceSlug,
postsSlug,
relationASlug,
relationBSlug,
} from './shared.js'
const filename = fileURLToPath(import.meta.url)
@@ -231,6 +234,155 @@ describe('database', () => {
const resFind = await payload.findByID({ id: res.id, collection: 'posts', depth: 0 })
expect(resFind.categoriesCustomID[0]).toBe(9999)
})
const describeUuidV7Adapter =
process.env.PAYLOAD_DATABASE === 'postgres-uuidv7' ||
process.env.PAYLOAD_DATABASE === 'sqlite-uuidv7'
? describe
: describe.skip
describeUuidV7Adapter('uuidv7', () => {
const createdRows: { collection: string; id: number | string }[] = []
const track = (collection: string, id: number | string) => {
createdRows.push({ collection, id })
}
afterEach(async () => {
for (const { collection, id } of [...createdRows].reverse()) {
try {
await payload.delete({
collection: collection as
| typeof customIDsSlug
| typeof postsSlug
| typeof relationASlug
| typeof relationBSlug,
id,
})
} catch {
// ignore: concurrent cleanup or FK already removed
}
}
createdRows.length = 0
})
it('should generate valid UUID with version 7', async () => {
const doc = await payload.create({
collection: postsSlug,
data: { title: 'uuidv7 test' },
})
track(postsSlug, doc.id)
expect(doc.id).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
)
expect(String(doc.id).charAt(14)).toBe('7')
})
it('should generate chronologically ordered IDs', async () => {
const doc1 = await payload.create({
collection: postsSlug,
data: { title: 'uuidv7 first' },
})
const doc2 = await payload.create({
collection: postsSlug,
data: { title: 'uuidv7 second' },
})
track(postsSlug, doc1.id)
track(postsSlug, doc2.id)
expect(doc2.id > doc1.id).toBe(true)
})
it('should findByID with uuidv7', async () => {
const created = await payload.create({
collection: postsSlug,
data: { title: 'uuidv7 findable' },
})
track(postsSlug, created.id)
const found = await payload.findByID({
collection: postsSlug,
id: created.id,
})
expect(found.id).toBe(created.id)
expect(found.title).toBe('uuidv7 findable')
})
it('should query with where clause on uuidv7 id', async () => {
const created = await payload.create({
collection: postsSlug,
data: { title: 'uuidv7 queryable' },
})
track(postsSlug, created.id)
const result = await payload.find({
collection: postsSlug,
where: { id: { equals: created.id } },
})
expect(result.docs).toHaveLength(1)
expect(result.docs[0]!.id).toBe(created.id)
})
it('should handle relationships with uuidv7 IDs', async () => {
const relA = await payload.create({
collection: relationASlug,
data: { title: 'uuidv7 rel A' },
})
const relB = await payload.create({
collection: relationBSlug,
data: {
title: 'uuidv7 rel B',
relationship: relA.id,
},
})
track(relationBSlug, relB.id)
track(relationASlug, relA.id)
const found = await payload.findByID({
collection: relationBSlug,
id: relB.id,
depth: 1,
})
expect(found.relationship).toBeDefined()
})
it('should work with versions and uuidv7 adapter', async () => {
const doc = await payload.create({
collection: customIDsSlug,
data: { title: 'v7 versioned' },
})
track(customIDsSlug, doc.id)
await payload.update({
collection: customIDsSlug,
id: doc.id,
data: { title: 'v7 versioned updated' },
})
const versions = await payload.findVersions({
collection: customIDsSlug,
where: { parent: { equals: doc.id } },
})
expect(versions.totalDocs).toBeGreaterThanOrEqual(1)
})
it('defaultIDType should be text for uuidv7', () => {
expect(payload.db.defaultIDType).toBe('text')
})
})
})
describe('timestamps', () => {
@@ -751,7 +903,7 @@ describe('database', () => {
let id: any = null
if (payload.db.name === 'mongoose') {
id = new mongoose.Types.ObjectId().toHexString()
} else if (payload.db.idType === 'uuid') {
} else if (payload.db.idType === 'uuid' || payload.db.idType === 'uuidv7') {
id = randomUUID()
} else {
id = 9999
@@ -766,7 +918,7 @@ describe('database', () => {
let id: any = null
if (payload.db.name === 'mongoose') {
id = new mongoose.Types.ObjectId().toHexString()
} else if (payload.db.idType === 'uuid') {
} else if (payload.db.idType === 'uuid' || payload.db.idType === 'uuidv7') {
id = randomUUID()
} else {
id = 99999
@@ -788,7 +940,7 @@ describe('database', () => {
let id: any = null
if (payload.db.name === 'mongoose') {
id = new mongoose.Types.ObjectId().toHexString()
} else if (payload.db.idType === 'uuid') {
} else if (payload.db.idType === 'uuid' || payload.db.idType === 'uuidv7') {
id = randomUUID()
} else {
id = 999999
@@ -2021,7 +2173,9 @@ describe('database', () => {
describe('local api', () => {
// sqlite cannot handle concurrent write transactions
if (
!['cosmosdb', 'firestore', 'sqlite', 'sqlite-uuid'].includes(process.env.PAYLOAD_DATABASE)
!['cosmosdb', 'firestore', 'sqlite', 'sqlite-uuid', 'sqlite-uuidv7'].includes(
process.env.PAYLOAD_DATABASE || '',
)
) {
it('should commit multiple operations in isolation', async () => {
const req = {
+111 -90
View File
@@ -138,7 +138,7 @@ export interface Config {
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: number;
defaultIDType: string;
};
fallbackLocale: ('false' | 'none' | 'null') | false | null | ('en' | 'es' | 'uk') | ('en' | 'es' | 'uk')[];
globals: {
@@ -188,7 +188,7 @@ export interface UserAuthOperations {
* via the `definition` "noTimeStamps".
*/
export interface NoTimeStamp {
id: number;
id: string;
title?: string | null;
}
/**
@@ -196,12 +196,12 @@ export interface NoTimeStamp {
* via the `definition` "categories".
*/
export interface Category {
id: number;
id: string;
title?: string | null;
simple?: (number | null) | Simple;
simple?: (string | null) | Simple;
hideout?: {
camera1?: {
time1Image?: (number | null) | Post;
time1Image?: (string | null) | Post;
};
};
updatedAt: string;
@@ -213,7 +213,7 @@ export interface Category {
* via the `definition` "simple".
*/
export interface Simple {
id: number;
id: string;
text?: string | null;
number?: number | null;
updatedAt: string;
@@ -224,9 +224,9 @@ export interface Simple {
* via the `definition` "posts".
*/
export interface Post {
id: number;
id: string;
title: string;
category?: (number | null) | Category;
category?: (string | null) | Category;
categoryID?:
| {
[k: string]: unknown;
@@ -238,16 +238,16 @@ export interface Post {
| null;
categoryTitle?: string | null;
categorySimpleText?: string | null;
categories?: (number | Category)[] | null;
categories?: (string | Category)[] | null;
categoriesCustomID?: (number | CategoriesCustomId)[] | null;
categoryPoly?: {
relationTo: 'categories';
value: number | Category;
value: string | Category;
} | null;
categoryPolyMany?:
| {
relationTo: 'categories';
value: number | Category;
value: string | Category;
}[]
| null;
categoryCustomID?: (number | null) | CategoriesCustomId;
@@ -255,11 +255,11 @@ export interface Post {
| (
| {
relationTo: 'categories';
value: number | Category;
value: string | Category;
}
| {
relationTo: 'simple';
value: number | Simple;
value: string | Simple;
}
)[]
| null;
@@ -267,11 +267,11 @@ export interface Post {
| (
| {
relationTo: 'categories';
value: number | Category;
value: string | Category;
}
| {
relationTo: 'simple';
value: number | Simple;
value: string | Simple;
}
)[]
| null;
@@ -301,11 +301,11 @@ export interface Post {
| (
| {
relationTo: 'categories';
value: number | Category;
value: string | Category;
}
| {
relationTo: 'simple';
value: number | Simple;
value: string | Simple;
}
)[]
| null;
@@ -367,7 +367,7 @@ export interface CategoriesCustomId {
* via the `definition` "simple-localized".
*/
export interface SimpleLocalized {
id: number;
id: string;
text?: string | null;
number?: number | null;
updatedAt: string;
@@ -378,7 +378,7 @@ export interface SimpleLocalized {
* via the `definition` "error-on-unnamed-fields".
*/
export interface ErrorOnUnnamedField {
id: number;
id: string;
groupWithinUnnamedTab: {
text: string;
};
@@ -390,7 +390,7 @@ export interface ErrorOnUnnamedField {
* via the `definition` "default-values".
*/
export interface DefaultValue {
id: number;
id: string;
title?: string | null;
defaultValue?: string | null;
array?:
@@ -417,7 +417,7 @@ export interface DefaultValue {
* via the `definition` "relation-a".
*/
export interface RelationA {
id: number;
id: string;
title?: string | null;
richText?: {
root: {
@@ -442,9 +442,9 @@ export interface RelationA {
* via the `definition` "relation-b".
*/
export interface RelationB {
id: number;
id: string;
title?: string | null;
relationship?: (number | null) | RelationA;
relationship?: (string | null) | RelationA;
richText?: {
root: {
type: string;
@@ -468,14 +468,14 @@ export interface RelationB {
* via the `definition` "pg-migrations".
*/
export interface PgMigration {
id: number;
relation1?: (number | null) | RelationA;
id: string;
relation1?: (string | null) | RelationA;
myArray?:
| {
relation2?: (number | null) | RelationB;
relation2?: (string | null) | RelationB;
mySubArray?:
| {
relation3?: (number | null) | RelationB;
relation3?: (string | null) | RelationB;
id?: string | null;
}[]
| null;
@@ -483,12 +483,12 @@ export interface PgMigration {
}[]
| null;
myGroup?: {
relation4?: (number | null) | RelationB;
relation4?: (string | null) | RelationB;
};
myBlocks?:
| {
relation5?: (number | null) | RelationA;
relation6?: (number | null) | RelationB;
relation5?: (string | null) | RelationA;
relation6?: (string | null) | RelationB;
id?: string | null;
blockName?: string | null;
blockType: 'myBlock';
@@ -502,10 +502,10 @@ export interface PgMigration {
* via the `definition` "custom-schema".
*/
export interface CustomSchema {
id: number;
id: string;
text?: string | null;
localizedText?: string | null;
relationship?: (number | RelationA)[] | null;
relationship?: (string | RelationA)[] | null;
select?: ('a' | 'b' | 'c')[] | null;
radio?: ('a' | 'b' | 'c') | null;
array?:
@@ -533,7 +533,7 @@ export interface CustomSchema {
* via the `definition` "places".
*/
export interface Place {
id: number;
id: string;
country?: string | null;
city?: string | null;
updatedAt: string;
@@ -544,7 +544,7 @@ export interface Place {
* via the `definition` "virtual-relations".
*/
export interface VirtualRelation {
id: number;
id: string;
postTitle?: string | null;
postsTitles?: string[] | null;
postCategoriesTitles?: string[] | null;
@@ -570,8 +570,8 @@ export interface VirtualRelation {
| boolean
| null;
postLocalized?: string | null;
post?: (number | null) | Post;
posts?: (number | Post)[] | null;
post?: (string | null) | Post;
posts?: (string | Post)[] | null;
customID?: (string | null) | CustomId;
customIDValue?: string | null;
updatedAt: string;
@@ -594,7 +594,7 @@ export interface CustomId {
* via the `definition` "fields-persistance".
*/
export interface FieldsPersistance {
id: number;
id: string;
text?: string | null;
textHooked?: string | null;
array?:
@@ -605,6 +605,15 @@ export interface FieldsPersistance {
textWithinRow?: string | null;
textWithinCollapsible?: string | null;
textWithinTabs?: string | null;
blockWithVirtual?:
| {
text?: string | null;
virtualField?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'blockWithVirtual';
}[]
| null;
updatedAt: string;
createdAt: string;
}
@@ -613,7 +622,7 @@ export interface FieldsPersistance {
* via the `definition` "fake-custom-ids".
*/
export interface FakeCustomId {
id: number;
id: string;
title?: string | null;
group?: {
id?: string | null;
@@ -629,11 +638,11 @@ export interface FakeCustomId {
* via the `definition` "relationships-migration".
*/
export interface RelationshipsMigration {
id: number;
relationship?: (number | null) | DefaultValue;
id: string;
relationship?: (string | null) | DefaultValue;
relationship_2?: {
relationTo: 'default-values';
value: number | DefaultValue;
value: string | DefaultValue;
} | null;
updatedAt: string;
createdAt: string;
@@ -643,7 +652,7 @@ export interface RelationshipsMigration {
* via the `definition` "compound-indexes".
*/
export interface CompoundIndex {
id: number;
id: string;
one?: string | null;
two?: string | null;
three?: string | null;
@@ -658,7 +667,7 @@ export interface CompoundIndex {
* via the `definition` "aliases".
*/
export interface Alias {
id: number;
id: string;
thisIsALongFieldNameThatCanCauseAPostgresErrorEvenThoughWeSetAShorterDBName?:
| {
nestedArray?:
@@ -678,7 +687,7 @@ export interface Alias {
* via the `definition` "blocks-docs".
*/
export interface BlocksDoc {
id: number;
id: string;
testBlocksLocalized?:
| {
text?: string | null;
@@ -703,7 +712,7 @@ export interface BlocksDoc {
* via the `definition` "unique-fields".
*/
export interface UniqueField {
id: number;
id: string;
slugField?: string | null;
updatedAt: string;
createdAt: string;
@@ -713,7 +722,7 @@ export interface UniqueField {
* via the `definition` "select-has-many".
*/
export interface SelectHasMany {
id: number;
id: string;
roles?: ('user' | 'admin' | 'editor')[] | null;
food?: ('apple' | 'bananabread' | 'banana')[] | null;
updatedAt: string;
@@ -724,7 +733,7 @@ export interface SelectHasMany {
* via the `definition` "virtual-linked-tenants".
*/
export interface VirtualLinkedTenant {
id: number;
id: string;
slug: string;
updatedAt: string;
createdAt: string;
@@ -734,9 +743,9 @@ export interface VirtualLinkedTenant {
* via the `definition` "virtual-linked-roles".
*/
export interface VirtualLinkedRole {
id: number;
project: number | VirtualLinkedProject;
tenant: number | VirtualLinkedTenant;
id: string;
project: string | VirtualLinkedProject;
tenant: string | VirtualLinkedTenant;
tenantSlug?: string | null;
updatedAt: string;
createdAt: string;
@@ -746,9 +755,9 @@ export interface VirtualLinkedRole {
* via the `definition` "virtual-linked-projects".
*/
export interface VirtualLinkedProject {
id: number;
id: string;
roles?: {
docs?: (number | VirtualLinkedRole)[];
docs?: (string | VirtualLinkedRole)[];
hasNextPage?: boolean;
totalDocs?: number;
};
@@ -760,7 +769,7 @@ export interface VirtualLinkedProject {
* via the `definition` "payload-kv".
*/
export interface PayloadKv {
id: number;
id: string;
key: string;
data:
| {
@@ -777,7 +786,7 @@ export interface PayloadKv {
* via the `definition` "users".
*/
export interface User {
id: number;
id: string;
updatedAt: string;
createdAt: string;
email: string;
@@ -802,23 +811,23 @@ export interface User {
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: number;
id: string;
document?:
| ({
relationTo: 'noTimeStamps';
value: number | NoTimeStamp;
value: string | NoTimeStamp;
} | null)
| ({
relationTo: 'categories';
value: number | Category;
value: string | Category;
} | null)
| ({
relationTo: 'simple';
value: number | Simple;
value: string | Simple;
} | null)
| ({
relationTo: 'simple-localized';
value: number | SimpleLocalized;
value: string | SimpleLocalized;
} | null)
| ({
relationTo: 'categories-custom-id';
@@ -826,43 +835,43 @@ export interface PayloadLockedDocument {
} | null)
| ({
relationTo: 'posts';
value: number | Post;
value: string | Post;
} | null)
| ({
relationTo: 'error-on-unnamed-fields';
value: number | ErrorOnUnnamedField;
value: string | ErrorOnUnnamedField;
} | null)
| ({
relationTo: 'default-values';
value: number | DefaultValue;
value: string | DefaultValue;
} | null)
| ({
relationTo: 'relation-a';
value: number | RelationA;
value: string | RelationA;
} | null)
| ({
relationTo: 'relation-b';
value: number | RelationB;
value: string | RelationB;
} | null)
| ({
relationTo: 'pg-migrations';
value: number | PgMigration;
value: string | PgMigration;
} | null)
| ({
relationTo: 'custom-schema';
value: number | CustomSchema;
value: string | CustomSchema;
} | null)
| ({
relationTo: 'places';
value: number | Place;
value: string | Place;
} | null)
| ({
relationTo: 'virtual-relations';
value: number | VirtualRelation;
value: string | VirtualRelation;
} | null)
| ({
relationTo: 'fields-persistance';
value: number | FieldsPersistance;
value: string | FieldsPersistance;
} | null)
| ({
relationTo: 'custom-ids';
@@ -870,52 +879,52 @@ export interface PayloadLockedDocument {
} | null)
| ({
relationTo: 'fake-custom-ids';
value: number | FakeCustomId;
value: string | FakeCustomId;
} | null)
| ({
relationTo: 'relationships-migration';
value: number | RelationshipsMigration;
value: string | RelationshipsMigration;
} | null)
| ({
relationTo: 'compound-indexes';
value: number | CompoundIndex;
value: string | CompoundIndex;
} | null)
| ({
relationTo: 'aliases';
value: number | Alias;
value: string | Alias;
} | null)
| ({
relationTo: 'blocks-docs';
value: number | BlocksDoc;
value: string | BlocksDoc;
} | null)
| ({
relationTo: 'unique-fields';
value: number | UniqueField;
value: string | UniqueField;
} | null)
| ({
relationTo: 'select-has-many';
value: number | SelectHasMany;
value: string | SelectHasMany;
} | null)
| ({
relationTo: 'virtual-linked-tenants';
value: number | VirtualLinkedTenant;
value: string | VirtualLinkedTenant;
} | null)
| ({
relationTo: 'virtual-linked-roles';
value: number | VirtualLinkedRole;
value: string | VirtualLinkedRole;
} | null)
| ({
relationTo: 'virtual-linked-projects';
value: number | VirtualLinkedProject;
value: string | VirtualLinkedProject;
} | null)
| ({
relationTo: 'users';
value: number | User;
value: string | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: number | User;
value: string | User;
};
updatedAt: string;
createdAt: string;
@@ -925,10 +934,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: number;
id: string;
user: {
relationTo: 'users';
value: number | User;
value: string | User;
};
key?: string | null;
value?:
@@ -948,7 +957,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: number;
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
@@ -1291,6 +1300,18 @@ export interface FieldsPersistanceSelect<T extends boolean = true> {
textWithinRow?: T;
textWithinCollapsible?: T;
textWithinTabs?: T;
blockWithVirtual?:
| T
| {
blockWithVirtual?:
| T
| {
text?: T;
virtualField?: T;
id?: T;
blockName?: T;
};
};
updatedAt?: T;
createdAt?: T;
}
@@ -1514,7 +1535,7 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
* via the `definition` "header".
*/
export interface Header {
id: number;
id: string;
itemsLvl1?:
| {
label?: string | null;
@@ -1547,7 +1568,7 @@ export interface Header {
* via the `definition` "global".
*/
export interface Global {
id: number;
id: string;
text?: string | null;
updatedAt?: string | null;
createdAt?: string | null;
@@ -1557,7 +1578,7 @@ export interface Global {
* via the `definition` "global-2".
*/
export interface Global2 {
id: number;
id: string;
text?: string | null;
updatedAt?: string | null;
createdAt?: string | null;
@@ -1567,7 +1588,7 @@ export interface Global2 {
* via the `definition` "global-3".
*/
export interface Global3 {
id: number;
id: string;
text?: string | null;
updatedAt?: string | null;
createdAt?: string | null;
@@ -1577,9 +1598,9 @@ export interface Global3 {
* via the `definition` "virtual-relation-global".
*/
export interface VirtualRelationGlobal {
id: number;
id: string;
postTitle?: string | null;
post?: (number | null) | Post;
post?: (string | null) | Post;
updatedAt?: string | null;
createdAt?: string | null;
}
+18
View File
@@ -86,6 +86,15 @@ export const allDatabaseAdapters = {
connectionString: process.env.POSTGRES_URL || process.env.DATABASE_URL || '${defaultPostgresUrl}',
},
})`,
'postgres-uuidv7': `
import { postgresAdapter } from '@payloadcms/db-postgres'
export const databaseAdapter = postgresAdapter({
idType: 'uuidv7',
pool: {
connectionString: process.env.POSTGRES_URL || process.env.DATABASE_URL || '${defaultPostgresUrl}',
},
})`,
'postgres-read-replica': `
import { postgresAdapter } from '@payloadcms/db-postgres'
@@ -146,6 +155,15 @@ export const databaseAdapter = contentAPIAdapter({
url: process.env.SQLITE_URL || process.env.DATABASE_URL || 'file:./payload.db',
}
})`,
'sqlite-uuidv7': `
import { sqliteAdapter } from '@payloadcms/db-sqlite'
export const databaseAdapter = sqliteAdapter({
idType: 'uuidv7',
client: {
url: process.env.SQLITE_URL || process.env.DATABASE_URL || 'file:./payload.db',
}
})`,
supabase: `
import { postgresAdapter } from '@payloadcms/db-postgres'
+1 -1
View File
@@ -113,7 +113,7 @@
"tempy": "^1.0.1",
"ts-essentials": "10.0.3",
"typescript": "5.7.3",
"uuid": "10.0.0",
"uuid": "11.1.0",
"vitest": "4.1.2",
"wrangler": "~4.61.1",
"zod": "^3.25.5"
+2 -1
View File
@@ -1812,7 +1812,8 @@ describe('Relationships', () => {
polymorphic: {
equals: {
relationTo: 'movies',
value: payload.db.idType === 'uuid' ? randomUUID() : 99,
value:
payload.db.idType === 'uuid' || payload.db.idType === 'uuidv7' ? randomUUID() : 99,
},
},
},
+43
View File
@@ -0,0 +1,43 @@
import type { PostgresAdapter } from '@payloadcms/db-postgres'
import type { DatabaseAdapterObj } from 'payload'
import path from 'path'
import { buildConfig } from 'payload'
import { fileURLToPath } from 'url'
const dirname = path.dirname(fileURLToPath(import.meta.url))
const { databaseAdapter } = (await import(path.resolve(dirname, '../databaseAdapter.js'))) as {
databaseAdapter: DatabaseAdapterObj<PostgresAdapter>
}
export default await buildConfig({
db: databaseAdapter,
secret: 'uuid-v7-test-secret',
admin: {
importMap: {
baseDir: path.resolve(dirname),
},
},
collections: [
{
slug: 'posts',
fields: [{ name: 'title', type: 'text' }],
},
{
slug: 'categories',
fields: [{ name: 'name', type: 'text' }],
},
{
slug: 'articles',
fields: [
{ name: 'title', type: 'text' },
{
name: 'category',
type: 'relationship',
relationTo: 'categories',
},
],
},
],
})
+142
View File
@@ -0,0 +1,142 @@
import type { Payload } from 'payload'
import path from 'path'
import { fileURLToPath } from 'url'
import { validate as uuidValidate } from 'uuid'
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'
import { initPayloadInt } from '../__helpers/shared/initPayloadInt.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
process.env.PAYLOAD_CONFIG_PATH = path.join(dirname, 'config.ts')
type UuidV7Collection = 'articles' | 'categories' | 'posts'
const describeUuidV7 = process.env.PAYLOAD_DATABASE === 'postgres-uuidv7' ? describe : describe.skip
describeUuidV7('UUID v7 idType (postgres)', () => {
let payload: Payload
/** Created in order; deleted in reverse (articles → categories → posts). */
const createdStack: { collection: UuidV7Collection; id: string }[] = []
const track = (collection: UuidV7Collection, id: string) => {
createdStack.push({ collection, id })
}
beforeAll(async () => {
;({ payload } = await initPayloadInt(dirname))
})
afterEach(async () => {
if (!payload) {
return
}
for (const { collection, id } of [...createdStack].reverse()) {
try {
await payload.delete({ collection, id })
} catch {
// ignore: already deleted or FK race
}
}
createdStack.length = 0
})
afterAll(async () => {
if (payload) {
await payload.destroy()
}
})
it('should expose uuidv7 adapter idType', () => {
expect(payload.db.idType).toBe('uuidv7')
})
it('should create a document with a valid UUID v7 default id', async () => {
const doc = await payload.create({
collection: 'posts',
data: { title: 'uuid v7 post' },
})
track('posts', String(doc.id))
expect(typeof doc.id).toBe('string')
expect(uuidValidate(doc.id)).toBe(true)
expect(doc.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)
expect(doc.id.charAt(14)).toBe('7')
})
it('should order ids lexicographically for consecutive creates', async () => {
const first = await payload.create({
collection: 'posts',
data: { title: 'first' },
})
const second = await payload.create({
collection: 'posts',
data: { title: 'second' },
})
track('posts', String(first.id))
track('posts', String(second.id))
expect(second.id > first.id).toBe(true)
})
it('should findByID with generated id', async () => {
const created = await payload.create({
collection: 'posts',
data: { title: 'find me' },
})
track('posts', String(created.id))
const found = await payload.findByID({
collection: 'posts',
id: created.id,
})
expect(found.id).toBe(created.id)
expect(found.title).toBe('find me')
})
it('should resolve relationship to category', async () => {
const category = await payload.create({
collection: 'categories',
data: { name: 'Cat A' },
})
const article = await payload.create({
collection: 'articles',
data: {
title: 'Article 1',
category: category.id,
},
depth: 1,
})
track('articles', String(article.id))
track('categories', String(category.id))
expect(article.category).toMatchObject({ id: category.id })
})
it('should query by id equals', async () => {
const created = await payload.create({
collection: 'posts',
data: { title: 'query by id' },
})
track('posts', String(created.id))
const res = await payload.find({
collection: 'posts',
where: { id: { equals: created.id } },
})
expect(res.docs).toHaveLength(1)
expect(res.docs[0].id).toBe(created.id)
})
})