f8b7ecf680eab20d6b7773093ebaf155760b16b5
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f8b7ecf680 |
fix(workflow): rebuild core workflowVersion rows in the 2-22 backfill (#22961)
## Problem
Syncing workflowVersion to core fails with `duplicate key value violates
unique constraint "IDX_WORKFLOW_VERSION_ONE_ACTIVE_PER_WORKFLOW"`. The
sync's `INSERT ... ON CONFLICT ("id")` only dedupes the primary key — it
can't dedupe `IDX_WORKFLOW_VERSION_ONE_ACTIVE_PER_WORKFLOW`
(`(workspaceId, workflowId) WHERE status='ACTIVE'`). When a leftover
ACTIVE core row exists for a `(workspaceId, workflowId)` with a stale id
(accumulated across the sync's earlier id schemes), inserting the new
active row collides, and the per-id purge misses it.
## Fix
Rewrite the 2-22 `backfill-workflow-version-core-links` command as a
**full rebuild**. Per workspace, in one raw-SQL transaction (core and
workspace schemas are the same database):
1. `DELETE` all `core.workflowVersion` for the workspace — clears every
stale/leftover row.
2. Insert a fresh own-id core row for **every** workspace version.
3. `UPDATE` `coreWorkflowVersionId` on **every** workspace record to its
new core id.
Because it wipes first and re-links all records, leftover ACTIVE rows
can't collide and no record is left pointing at a deleted core row — so
the dual-write's `linked → update` path stays correct afterward.
## Test (local)
Fresh reset, and a reproduced dirty state (leftover ACTIVE core row with
a stale id + a stale link + an unlinked record):
- rebuild runs with no `ONE_ACTIVE` / duplicate-key error,
- leftover wiped, stale link replaced, every record re-linked to a fresh
own-id row,
- 0 dangling/unlinked, no duplicate core rows, exactly one ACTIVE core
version per workflow.
Typecheck + lint + oxfmt clean.
## Note
The 2-20 `backfill-workflow-version-to-core` can still log per-workspace
conflicts on already-dirty instances, but they're non-fatal (the
iterator continues) and this rebuild corrects the end state. The
dual-write (`upsertToCore`) is unchanged and works on the clean data
this produces.
|
||
|
|
988f8ff900 |
feat(workflow): provision coreWorkflowVersionId on existing workspaces and link them (#22944)
Stacked on the write-back guard hotfix (#22940). Completes the version soft-ref for workspaces that predate the `coreWorkflowVersionId` field. ## Why New standard fields aren't auto-synced to existing workspaces; they're only built at workspace creation or added by an explicit upgrade command. Deployed 2.20/2.21 instances also carry **legacy core rows with `id = record.id`** (the pre-soft-ref shared-UUID model, from the already-run #22663 backfill). The original backfill has already run there and won't re-run (tracking is by command name), so the migration to soft-ref has to be **new appended commands**. ## What (two appended 2-22 workspace commands) 1. `add-workflow-version-core-soft-ref-field` (`1784193206000`): adds the `coreWorkflowVersionId` system field to existing workspaces missing it (flat-entity migration, `add-message-campaign-stat-fields` pattern). Idempotent, dry-run aware, skips workspaces without the `workflowVersion` object. 2. `backfill-workflow-version-core-links` (`1784193207000`): re-runs the sync (`upsertToCore`). For each version, `upsertToCore` **purges the legacy shared-id core row** (`id === record id`) then upserts a deterministic own-id row and writes the link back onto the workspace record. Targeted per-id delete — it does not wipe unrelated core rows. Ordering: both run after the original 2-20 backfill. On instances that run 2-20 fresh (e.g. 2.19 → 2.22) that backfill creates core rows and — via the hotfix guard — skips the write-back until the field exists; command 1 provisions the field; command 2 purges + relinks. On already-migrated 2.21 instances the 2-20 backfill won't re-run, so command 2 is what clears their shared-id rows. The same per-id purge in `upsertToCore` also covers the dual-write path: between the 2.22 deploy and command 2 running, an edited version would otherwise collide with its shared-id row on the one-active-per-workflow index. Scope: version side only. The workflow-side equivalent (`coreWorkflowId`) ships with the workflow-side sync PR; `core.workflow` was never backfilled in prod, so it has no legacy shared-id rows. ## Test - Unit: guard skips write-back when the field is absent; runs it when present. - Happy path (fresh reset): original backfill → add-field no-op → link → 4/4 linked, own ids, 0 duplicates. - Deployed migration (simulated 2.21: shared-id ACTIVE core rows + field removed): add-field re-provisions → link purges the shared-id rows and rebuilds → records linked to own-id rows, 0 duplicates, exactly one ACTIVE core version per workflow (index intact), no collision. - Idempotent re-run: still 4 core rows, links resolve. - Typecheck + lint + oxfmt clean. |
||
|
|
f4ff234db8 |
feat: make record avatar/icon resolution data-driven via a configurable image identifier field (#22644)
## Summary Today the avatar/icon shown for a record is hardcoded per object — Company pulls a favicon from its domain link, Person uses `avatarUrl`, etc. This PR replaces that hardcoding with a generic, data-driven abstraction based on a configurable **image identifier field** on each object's metadata (mirroring the existing **label identifier** concept). An object's image identifier can point to: - a **`FILES`** field → the uploaded image is used directly (rounded avatar), or - a **`LINKS`** field → a favicon is derived from the primary URL via the Twenty icons service (squared avatar), gated by `ALLOW_REQUESTS_TO_TWENTY_ICONS`. This lets any object type (Opportunity, a custom "Listing", etc.) define its own avatar/icon without code changes, and makes the field configurable/overridable for standard objects. ## ❓ Open question: also allow `TEXT` → direct image URL? Right now the image identifier is restricted to `FILES` (uploaded file) and `LINKS` (favicon). We deliberately left out `TEXT` → **direct image URL** (e.g. an imported/synced photo URL stored in a text field). There's precedent for it — Person's avatar was originally a `TEXT` `avatarUrl`, and WorkspaceMember still is — and it's unambiguous (a `TEXT` field has no favicon-vs-image ambiguity, and selecting it as the image identifier is itself the declaration of intent). It's a small, clean extension: - add `TEXT` to the allowed image-identifier types, - add an explicit `TEXT → raw URL` case - `getAvatarType`: `TEXT → rounded`. Caveats: it relies on admin assertion that the text values are image URLs (no data-level guarantee), and external image URLs load third-party content in the browser (IP-leak/hotlinking, same as favicons — a proxy/cache would be the more robust long-term answer). ### ✅ Resolution Decision: **we will not support `TEXT` as an image identifier.** Image identifiers stay restricted to `FILES` and `LINKS`, and any other type fails closed (returns no avatar) on both the frontend and backend. Instead, the legacy items that still rely on a `TEXT` avatar — Person's deprecated `avatarUrl` and WorkspaceMember's `avatarUrl` — will be migrated to `FILE` fields in a follow-up PR. Until then, WorkspaceMember remains an exception (its `avatarUrl` still resolves through the existing CorePicture path), and legacy Person `avatarUrl` values that haven't been migrated will show initials placeholders. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22644?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. --> |