Skip to content

Commit 73937b7

Browse files
chore: localize status migration work (#14862)
Adds migration logic. ### 1. Create blank migration file ```bash payload migrate:create localize_status ``` ### 2. Add the migration code **PostgreSQL / SQLite:** ```typescript import type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/db-postgres' import { sql } from '@payloadcms/db-postgres' import { localizeStatus } from 'payload/migrations' export async function up({ db, payload }: MigrateUpArgs): Promise<void> { await localizeStatus.up({ collectionSlug: 'posts', // 👈 Change to your collection db, payload, sql, }) } export async function down({ db, payload }: MigrateDownArgs): Promise<void> { await localizeStatus.down({ collectionSlug: 'posts', db, payload, sql, }) } ``` **MongoDB:** ```typescript import type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/db-mongodb' import { localizeStatus } from 'payload/migrations' export async function up({ payload }: MigrateUpArgs): Promise<void> { await localizeStatus.up({ collectionSlug: 'posts', // 👈 Change to your collection payload, }) } export async function down({ payload }: MigrateDownArgs): Promise<void> { await localizeStatus.down({ collectionSlug: 'posts', payload, }) } ``` ##### Related PRs in this feature - [feat: adds versions.drafts.localizeStatus and allows unpublish per‑locale #14667](#14667) - [feat(ui): experimental localize metadata UI #14699](#14699) - [chore: localize status migration work #14862](#14862) --------- Co-authored-by: Jessica Chowdhury <[email protected]>
1 parent 9d88485 commit 73937b7

File tree

19 files changed

+2754
-80
lines changed

19 files changed

+2754
-80
lines changed

packages/payload/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@
6565
"types": "./src/exports/i18n/*.ts",
6666
"default": "./src/exports/i18n/*.ts"
6767
},
68+
"./migrations": {
69+
"import": "./src/exports/migrations.ts",
70+
"types": "./src/exports/migrations.ts",
71+
"default": "./src/exports/migrations.ts"
72+
},
6873
"./__testing__/predefinedMigration": {
6974
"import": "./src/__testing__/predefinedMigration.js",
7075
"default": "./src/__testing__/predefinedMigration.js"
@@ -181,6 +186,11 @@
181186
"types": "./dist/exports/i18n/*.d.ts",
182187
"default": "./dist/exports/i18n/*.js"
183188
},
189+
"./migrations": {
190+
"import": "./dist/exports/migrations.js",
191+
"types": "./dist/exports/migrations.d.ts",
192+
"default": "./dist/exports/migrations.js"
193+
},
184194
"./__testing__/predefinedMigration": {
185195
"import": "./dist/__testing__/predefinedMigration.js",
186196
"default": "./dist/__testing__/predefinedMigration.js"
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Template for localizeStatus migration
3+
* Transforms version._status from single value to per-locale object
4+
*/
5+
6+
export const localizeStatusTemplate = (options: {
7+
collectionSlug?: string
8+
dbType: 'mongodb' | 'postgres' | 'sqlite'
9+
globalSlug?: string
10+
}): string => {
11+
const { collectionSlug, dbType, globalSlug } = options
12+
const entity = collectionSlug
13+
? `collectionSlug: '${collectionSlug}'`
14+
: `globalSlug: '${globalSlug}'`
15+
16+
if (dbType === 'mongodb') {
17+
return `import { MigrateUpArgs, MigrateDownArgs } from '@payloadcms/db-mongodb'
18+
import { localizeStatus } from 'payload'
19+
20+
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
21+
await localizeStatus.up({
22+
${entity},
23+
payload,
24+
req,
25+
})
26+
}
27+
28+
export async function down({ payload, req }: MigrateDownArgs): Promise<void> {
29+
await localizeStatus.down({
30+
${entity},
31+
payload,
32+
req,
33+
})
34+
}
35+
`
36+
}
37+
38+
// SQL databases (Postgres, SQLite)
39+
return `import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-${dbType}'
40+
import { localizeStatus } from 'payload'
41+
42+
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
43+
await localizeStatus.up({
44+
${entity},
45+
db,
46+
payload,
47+
req,
48+
sql,
49+
})
50+
}
51+
52+
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
53+
await localizeStatus.down({
54+
${entity},
55+
db,
56+
payload,
57+
req,
58+
sql,
59+
})
60+
}
61+
`
62+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Exports for Payload migrations
3+
*
4+
* This module provides migration utilities that users can import in their migration files.
5+
*
6+
* @example
7+
* ```ts
8+
* import { localizeStatus } from 'payload/migrations'
9+
*
10+
* export async function up({ payload }) {
11+
* await localizeStatus.up({
12+
* collectionSlug: 'posts',
13+
* payload,
14+
* })
15+
* }
16+
* ```
17+
*/
18+
19+
export { localizeStatus } from '../versions/migrations/localizeStatus/index.js'

packages/payload/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1810,6 +1810,11 @@ export { getQueryDraftsSort } from './versions/drafts/getQueryDraftsSort.js'
18101810
export { enforceMaxVersions } from './versions/enforceMaxVersions.js'
18111811
export { getLatestCollectionVersion } from './versions/getLatestCollectionVersion.js'
18121812
export { getLatestGlobalVersion } from './versions/getLatestGlobalVersion.js'
1813+
export { localizeStatus } from './versions/migrations/localizeStatus/index.js'
1814+
export type {
1815+
MongoLocalizeStatusArgs,
1816+
SqlLocalizeStatusArgs,
1817+
} from './versions/migrations/localizeStatus/index.js'
18131818
export { saveVersion } from './versions/saveVersion.js'
18141819
export type { SchedulePublishTaskInput } from './versions/schedule/types.js'
18151820
export type { SchedulePublish, TypeWithVersion } from './versions/types.js'
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
# localizeStatus Migration
2+
3+
Migrate your existing version data to support per-locale draft/published status when enabling `localizeStatus: true`.
4+
5+
**Supported databases**: PostgreSQL, SQLite, MongoDB
6+
7+
## Quick Start
8+
9+
### 1. Create a migration file
10+
11+
```bash
12+
payload migrate:create localize_status
13+
```
14+
15+
### 2. Add the migration code
16+
17+
**PostgreSQL / SQLite:**
18+
19+
```typescript
20+
import type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/db-postgres'
21+
import { sql } from '@payloadcms/db-postgres'
22+
import { localizeStatus } from 'payload/migrations'
23+
24+
export async function up({ db, payload }: MigrateUpArgs): Promise<void> {
25+
await localizeStatus.up({
26+
collectionSlug: 'posts', // 👈 Change to your collection
27+
db,
28+
payload,
29+
sql,
30+
})
31+
}
32+
33+
export async function down({ db, payload }: MigrateDownArgs): Promise<void> {
34+
await localizeStatus.down({
35+
collectionSlug: 'posts',
36+
db,
37+
payload,
38+
sql,
39+
})
40+
}
41+
```
42+
43+
**MongoDB:**
44+
45+
```typescript
46+
import type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/db-mongodb'
47+
import { localizeStatus } from 'payload/migrations'
48+
49+
export async function up({ payload }: MigrateUpArgs): Promise<void> {
50+
await localizeStatus.up({
51+
collectionSlug: 'posts', // 👈 Change to your collection
52+
payload,
53+
})
54+
}
55+
56+
export async function down({ payload }: MigrateDownArgs): Promise<void> {
57+
await localizeStatus.down({
58+
collectionSlug: 'posts',
59+
payload,
60+
})
61+
}
62+
```
63+
64+
**For globals**, use `globalSlug` instead:
65+
66+
```typescript
67+
await localizeStatus.up({
68+
globalSlug: 'settings', // 👈 Your global slug
69+
payload,
70+
})
71+
```
72+
73+
### 3. Run the migration
74+
75+
```bash
76+
payload migrate
77+
```
78+
79+
## What it does
80+
81+
### BEFORE (old schema)
82+
83+
- **One status for all locales**: When you publish, ALL locales are published
84+
- No way to have draft content in one locale while another is published
85+
86+
### AFTER (new schema)
87+
88+
- **Per-locale status**: Each locale can be draft or published independently
89+
- Full control over which locales are published at any time
90+
91+
## Migration behavior
92+
93+
The migration processes your version history chronologically to determine the correct status for each locale:
94+
95+
1. **Published with specific locale** (`publishedLocale: 'en'`)
96+
97+
- That locale becomes 'published'
98+
- Other locales remain 'draft'
99+
100+
2. **Published without locale** (all locales)
101+
102+
- All locales become 'published'
103+
104+
3. **Draft save**
105+
- All locales become 'draft' (unpublish everything)
106+
107+
### Example
108+
109+
Version history:
110+
111+
1. V1: Publish all → `{ en: 'published', es: 'published', de: 'published' }`
112+
2. V2: Draft save → `{ en: 'draft', es: 'draft', de: 'draft' }`
113+
3. V3: Publish EN only → `{ en: 'published', es: 'draft', de: 'draft' }`
114+
115+
## When to use this
116+
117+
Use this migration when:
118+
119+
1. ✅ You have existing collections with `versions.drafts` enabled
120+
2. ✅ You want to enable `versions.drafts.localizeStatus: true`
121+
3. ✅ You need to preserve existing version history
122+
123+
Don't use this if:
124+
125+
- Starting a fresh project (just enable `localizeStatus: true` from the start)
126+
- Collection doesn't have versions enabled
127+
- Collection isn't localized
128+
129+
## Safety
130+
131+
### ⚠️ BACKUP YOUR DATABASE FIRST
132+
133+
This migration modifies your database schema:
134+
135+
- **PostgreSQL/SQLite**: Drops `version__status` column, adds `_status` to locales table
136+
- **MongoDB**: Transforms `version._status` from string to object
137+
- **Preserves**: `published_locale` column (needed for rollback)
138+
139+
**Create a backup before running:**
140+
141+
```bash
142+
# PostgreSQL
143+
pg_dump your_database > backup_before_migration.sql
144+
145+
# MongoDB
146+
mongodump --db your_database --out ./backup_before_migration
147+
```
148+
149+
### Migration guarantees
150+
151+
**Idempotent**: Safe to run multiple times (skips if already migrated)
152+
**Validated**: Checks schema before proceeding
153+
**Chronological**: Processes versions oldest-first for accuracy
154+
**Rollback**: Includes `down()` to revert changes
155+
156+
## Enable localizeStatus in your config
157+
158+
After migrating, enable the feature:
159+
160+
```typescript
161+
// Before
162+
{
163+
slug: 'posts',
164+
versions: {
165+
drafts: true,
166+
},
167+
}
168+
169+
// After
170+
{
171+
slug: 'posts',
172+
versions: {
173+
drafts: {
174+
localizeStatus: true,
175+
},
176+
},
177+
}
178+
```
179+
180+
## Rollback
181+
182+
To revert the migration:
183+
184+
```bash
185+
payload migrate:down
186+
```
187+
188+
**Note**: Rollback uses "ANY locale published = globally published" logic, so some granularity may be lost.
189+
190+
## Troubleshooting
191+
192+
### "version\_\_status column not found"
193+
194+
**Cause**: Migration already run, or column doesn't exist.
195+
196+
**Solution**: Check if already migrated. If yes, no action needed.
197+
198+
### Migration completes but data looks wrong
199+
200+
**Cause**: `publishedLocale` field may have been null in original data.
201+
202+
**Solution**: Review version history in database. The migration respects what's recorded in your data.
203+
204+
### Want to migrate multiple collections?
205+
206+
Call the migration multiple times:
207+
208+
```typescript
209+
export async function up({ db, payload }: MigrateUpArgs): Promise<void> {
210+
await localizeStatus.up({ collectionSlug: 'posts', db, payload, sql })
211+
await localizeStatus.up({ collectionSlug: 'articles', db, payload, sql })
212+
await localizeStatus.up({ globalSlug: 'settings', db, payload, sql })
213+
}
214+
```
215+
216+
## Pre-flight checklist
217+
218+
Before running:
219+
220+
- [ ] Database backup created
221+
- [ ] Tested on staging/development database
222+
- [ ] Verified version data looks correct
223+
- [ ] `localizeStatus: true` ready to enable in config
224+
- [ ] Reviewed expected behavior
225+
- [ ] Rollback plan ready
226+
227+
## Support
228+
229+
Issues? Check [GitHub](https://github.com/payloadcms/payload/issues) or the [Discord community](https://discord.com/invite/payload).

0 commit comments

Comments
 (0)