38fbff465f
Follow-up to #22417, per [this thread](https://github.com/twentyhq/twenty/pull/22417#discussion_r3512187719): migrate the `2-20/README.md` placeholder into a real command using the `TWENTY_NEXT_VERSIONS` mechanism. ### What - Add `DropMetadataStandardOverridesColumnFastInstanceCommand`, registered against `2.20.0`. It boots (`2.20.0` is in `TWENTY_ALL_VERSIONS`) but stays **dormant** — the upgrade sequence only runs `TWENTY_CROSS_UPGRADE_SUPPORTED_VERSIONS` (previous + current), so it never executes during the 2.19 deploy and activates automatically when `nx version:bump` promotes 2.20 to current. - Name constant + unit test (SQL parity, registration against `2.20.0`, name-constant parity). - Register it in `instance-commands.constant.ts`. - Update the `standardOverrides` `@deprecated` comments on object/field metadata to point at the shipped command. - Delete `2-20/README.md`. - Document the "ship a command for a future version" flow in `docs/UPGRADE_COMMANDS.md` and `.cursor/rules/server-migrations.mdc` (the mechanism was previously undocumented). ### Note / correction to the README's plan The old README implied both the command **and** `@WasRemovedInUpgrade` could be added at 2.20 time. Only the command can ship now: the decorator's validator runs against the active sequence, so referencing a still-dormant 2.20 step fails boot with `unknown-step-name`. So the entity keeps its `WasRemovedInUpgrade<T>` type wrapper for now; the decorator gets wired (one line, via the name constant) once 2.20 is current — same deferred-drop shape as `isUIReadOnly`. ### Verification Could not run `jest`/`typecheck`/`lint` in this environment: `yarn install` is blocked by egress policy on a git-based transitive dep (`github.com/electron/node-gyp.git`). Verified by review against the sibling 2-19 add-column and 2-12 drop commands. **Please let CI run before merge.** https://claude.ai/code/session_01KMArJvdEmsX3eAmJLbS1b6 --- _Generated by [Claude Code](https://claude.ai/code/session_01KMArJvdEmsX3eAmJLbS1b6)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22448?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
130 lines
4.9 KiB
Markdown
130 lines
4.9 KiB
Markdown
# Upgrade Commands
|
|
|
|
The upgrade process relies on two types of commands:
|
|
|
|
- **Instance commands** — schema and data migrations that run once at the instance level (replacing raw TypeORM migrations).
|
|
- **Workspace commands** — commands that iterate over all active or suspended workspaces to apply per-workspace changes.
|
|
|
|
Both are registered via decorators and automatically discovered by the upgrade pipeline.
|
|
|
|
## Instance Commands
|
|
|
|
### Generating an instance command
|
|
|
|
```bash
|
|
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
|
|
```
|
|
|
|
This generates a timestamped file and auto-registers it in `instance-commands.constant.ts` — do not edit that file manually.
|
|
|
|
### Fast instance commands
|
|
|
|
Fast commands run immediately during the upgrade. They are used for schema changes that could introduce breaking inconsistencies between the database and the server if delayed.
|
|
|
|
A fast command implements `FastInstanceCommand` and provides `up` / `down` methods:
|
|
|
|
```ts
|
|
@RegisteredInstanceCommand('1.22.0', 1775758621017)
|
|
export class AddWorkspaceIdToTotoFastInstanceCommand
|
|
implements FastInstanceCommand
|
|
{
|
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(
|
|
`ALTER TABLE "core"."toto" ADD "workspaceId" uuid`,
|
|
);
|
|
}
|
|
|
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(
|
|
`ALTER TABLE "core"."toto" DROP COLUMN "workspaceId"`,
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Slow instance commands
|
|
|
|
Slow commands are used when a potentially long-running data migration must happen before the schema change. They only run when the `--include-slow` flag is passed.
|
|
|
|
A slow command implements `SlowInstanceCommand`, which extends `FastInstanceCommand` with an additional `runDataMigration` method that executes before `up`:
|
|
|
|
```ts
|
|
@RegisteredInstanceCommand('1.22.0', 1775758621018, { type: 'slow' })
|
|
export class BackfillWorkspaceIdSlowInstanceCommand
|
|
implements SlowInstanceCommand
|
|
{
|
|
async runDataMigration(dataSource: DataSource): Promise<void> {
|
|
// Backfill logic (can be slow — e.g. iterating over workspaces, cache recomputation)
|
|
}
|
|
|
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(
|
|
`ALTER TABLE "core"."toto" ALTER COLUMN "workspaceId" SET NOT NULL`,
|
|
);
|
|
}
|
|
|
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(
|
|
`ALTER TABLE "core"."toto" ALTER COLUMN "workspaceId" DROP NOT NULL`,
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
A common pattern is to pair a **fast** command (add a nullable column) with a **slow** command (backfill existing rows, then set `NOT NULL`).
|
|
|
|
## Workspace Commands
|
|
|
|
Workspace commands run per-workspace logic across all active or suspended workspaces. They are registered with the `@RegisteredWorkspaceCommand` decorator alongside nest-commander's `@Command` decorator:
|
|
|
|
```ts
|
|
@RegisteredWorkspaceCommand('1.22.0', 1780000002000)
|
|
@Command({
|
|
name: 'upgrade:1-22:backfill-standard-skills',
|
|
description:
|
|
'Backfill standard skills for existing workspaces',
|
|
})
|
|
export class BackfillStandardSkillsCommand
|
|
extends ActiveOrSuspendedWorkspaceCommandRunner
|
|
{
|
|
constructor(
|
|
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
|
// inject any services you need
|
|
) {
|
|
super(workspaceIteratorService);
|
|
}
|
|
|
|
override async runOnWorkspace({
|
|
workspaceId,
|
|
options,
|
|
}: RunOnWorkspaceArgs): Promise<void> {
|
|
// Per-workspace logic goes here
|
|
// options.dryRun, options.verbose are available for free
|
|
}
|
|
}
|
|
```
|
|
|
|
The base class `ActiveOrSuspendedWorkspaceCommandRunner` handles workspace iteration and provides `--dry-run`, `--verbose`, and workspace filter options automatically.
|
|
|
|
## Execution Order
|
|
|
|
Within a given version of Twenty, the upgrade pipeline runs commands in this order, sorted by timestamp within each group:
|
|
|
|
1. **Instance fast** commands
|
|
2. **Instance slow** commands
|
|
3. **Workspace commands**
|
|
|
|
Workspace commands are executed sequentially across all active/suspended workspaces.
|
|
|
|
## Shipping a command for a future version (deferred drops)
|
|
|
|
You can write a command for a version listed in `TWENTY_NEXT_VERSIONS` — typically the second half of a zero-downtime migration, e.g. dropping a column one release after its replacement ships. Pass the target version to the generator:
|
|
|
|
```bash
|
|
npx nx run twenty-server:database:migrate:generate --name <name> --type fast --version 2.20.0
|
|
```
|
|
|
|
It registers and boots (versions are validated against `TWENTY_ALL_VERSIONS`) but stays **dormant** — the sequence only runs `TWENTY_CROSS_UPGRADE_SUPPORTED_VERSIONS` (previous + current). It activates automatically when `nx version:bump` promotes the version to current.
|
|
|
|
**Caveat:** `@WasRemovedInUpgrade` / `@WasIntroducedInUpgrade` are validated against the active sequence, so a decorator pointing at a still-dormant next-version command fails boot with `unknown-step-name`. For a deferred drop, keep the entity's `WasRemovedInUpgrade<T>` type wrapper now and add the decorator only once the version is current.
|