a8a8bbb2ed86305af2bc1fab19afef5fe0aad9a1
2397 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a8a8bbb2ed |
feat(workflow): add offset to Find Records node for pagination (#21484)
<img width="471" height="362" alt="Capture d’écran 2026-06-12 à 15 38 45" src="https://github.com/user-attachments/assets/9656d3a6-6f56-4587-add6-55c0a0a32482" /> ## Summary The workflow Find Records (search) node previously exposed only `objectName`, `filter`, `sort`, and `limit` (capped at `QUERY_MAX_RECORDS` = 200), with no way to page beyond the first page of results. This adds an optional **Offset** to the node so a workflow can fetch an arbitrary page (`offset = pageIndex * limit`) while keeping the same filter and sort. The underlying `FindRecordsService` already accepts `offset` (it forwards it to the query runner's `skip`, and stabilizes ordering with an `id` tiebreaker), so this change just threads `offset` through the remaining layers: - `workflowFindRecordsActionSettingsSchema` (shared zod schema) — new optional `offset` - `FindRecordsInput` type — new optional `offset?: number` - `find-records.workflow-action.ts` — forwards `offset` to `FindRecordsService.execute` - `WorkflowEditActionFindRecords.tsx` — new "Offset" number input (non-negative, defaults to 0) with form state + persistence - Default `FIND_RECORDS` step settings — `offset: 0` ### Notes / non-goals - Offset-only, single page: the node returns one page. Looping over all pages inside one run is not included (the Iterator action loops a static array and cannot re-query). The node output already returns `totalCount`, so a workflow can compute total pages as `ceil(totalCount / limit)`. - Offset on very large/changing datasets can be slow or skip/duplicate rows; cursor/keyset pagination would be a future follow-up. ## Test plan - [x] Create a Find Records node, set Limit=50, Offset=0 → returns first page - [x] Set Offset=50 with the same filter/sort → returns the second page (no overlap) - [x] Negative offset shows a validation error and is not saved - [x] Existing Find Records nodes (no offset stored) still run, defaulting to offset 0 - [x] Typecheck/lint pass in CI <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21484?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. --> |
||
|
|
49026a7368 |
chore: bump version to 2.13.0 (#21492)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21492?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. --> Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
7b6b624041 |
fix(server): bypass stale workspace cache when resolving currentUser during onboarding (#21461)
## Context On twenty-main (multi-replica), signing up and naming a workspace lands on a black screen at `/create/profile`; a manual refresh fixes it. From the network trace: the post-activation `currentUser` response carries `onboardingStatus: PROFILE_CREATION` (fresh) together with a non-ACTIVE `currentWorkspace` and `workspaceMember: null` (stale). ## Root cause `activateWorkspace` invalidates the core entity cache only on the instance that served the mutation. When the follow-up `currentUser` query is routed to a sibling instance, the auth context carries a memoized pre-activation workspace snapshot. A stale transient workspace cascades: - `workspaceMember`/`workspaceMembers` resolve to null/empty (`loadWorkspaceMember` skips non-active workspaces), permissions fall back to defaults - the client's metadata store never loads (`MinimalMetadataLoadEffect` skips non-active workspaces), so `MinimalMetadataGater` shows the loading skeleton forever on `/create/profile` #20322 fixed the same staleness for `onboardingStatus` by reading the workspace fresh from the database in the resolver — which is why the status is fresh while the workspace object isn't, and why the client navigates to a page it can't render. #21480 bounds the staleness window to the designed 10s (absolute memoizer TTL), but signup lives entirely inside that window: the client reads `currentUser` ~1s after `activateWorkspace` and never refetches while stuck. ## Fix Apply the #20322 approach at the workspace status resolution layer: `UserService.refreshWorkspaceIfPendingOrOngoingCreation` re-reads the workspace from the database when the auth-context copy is in a transient activation status (`PENDING_CREATION`/`ONGOING_CREATION`). Used in: - `UserResolver.currentUser` — fresh `currentWorkspace` and permissions - `UserService.loadWorkspaceMember` / `loadWorkspaceMembers` — covers the `workspaceMember`/`workspaceMembers` resolve fields No-op for active workspaces; the extra database read only happens for workspaces mid-creation. ## Test plan - Full `user.service.spec.ts` suite passes; lint and format clean. - After deploy to twenty-main: sign up, name the workspace, verify `/create/profile` renders the profile form with a populated `workspaceMember` and ACTIVE `currentWorkspace` without refreshing. |
||
|
|
fefd9d7704 |
feat(workflow) - Add validation layer (#21422)
Add workflow validation framework and consolidate output schema types/search logic into twenty-shared This PR introduces a comprehensive workflow validation system that catches configuration errors at build-time, and consolidates the fragmented output-schema type definitions and variable-search logic from the front-end into twenty-shared **Workflow validation** — A new system that checks workflows for errors before activation: graph connectivity (unreachable steps, dangling references), step parameter schemas (via Zod), variable references (typos, wrong step order), and workspace metadata (non-existent objects). Returns structured errors/warnings with "did you mean?" suggestions. Runs automatically after create_complete_workflow and update_workflow_version_step, and is also available as a standalone validate_workflow tool. **Output schema consolidation** — Moves all output schema types and the variable-search logic from scattered front-end files into twenty-shared, replacing ~800 lines of duplicated per-schema-type code with a single unified searchVariableInOutputSchema dispatcher. To do : - validation on CODE and AGENT step --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
cfb9772179 |
feat(server): convert view to overridable entity (#21436)
## Context Every entity created as a side effect of object creation must support the overridable pattern (`isActive` + `overrides` + override routing) before we can re-own side effects to their true application. Starting with View. viewField, viewFieldGroup, pageLayoutTab and pageLayoutWidget already extend `OverridableEntity`. This PR brings `view` to the same pattern. ## What this does - `ViewEntity` now extends `OverridableEntity<ViewOverrides>` (adds `isActive` boolean + `overrides` jsonb). All editable view properties are overridable; the 3 fieldMetadata foreign keys are converted to/from universal identifiers like viewField's `viewFieldGroupId`. - **Update**: mutations on a view not owned by the caller (e.g. standard views like "All Companies") are written into `overrides` instead of mutating the row. Reads merge overrides in the DTO. - **Delete/destroy**: views not owned by the caller are deactivated (`isActive = false`) instead of deleted. ~~- **INDEX invariant**: `key = INDEX` views can only be created via object-creation side effect. The API now rejects creating, deleting or destroying INDEX views (object-deletion cascade is unaffected). This was not really needed for this migration but was flagged during implementation.~~ - **Front**: views with `isActive = false` are filtered out of the views selector. - Fast instance command adds the two columns (`2-12-instance-command-fast-...-view-overridable-entity.ts`). ## Notes - Custom (caller-owned) views behave exactly as before: direct updates, soft delete. - View-group side effects (kanban groups) are computed on the override-merged view so overridden `mainGroupByFieldMetadataId` works. |
||
|
|
41cdd83367 |
fix(ai): correct RICH_TEXT and MORPH_RELATION record filter operators (#21106)
## Problem
The AI find-records tool generates filter schemas via
`generateFieldFilterZodSchema`. `RICH_TEXT` currently shares the `TEXT`
case, so the agent is told it can use scalar text operators
(`like`/`ilike`/`startsWith`/`endsWith`/`eq`/…) directly on a rich-text
field.
But `RICH_TEXT` is a **composite** type (`markdown` + `blocknote`
sub-fields, see `rich-text.composite-type.ts`). Applying a scalar
operator to the composite root throws at query time:
```
ERROR [FindRecordsService] Failed to find records: Object person doesn't have any "ilike" field.
ERROR [FindRecordsService] Failed to find records: Sub field "ilike" not found for composite type: RICH_TEXT
```
`FindRecordsService` catches and returns `success: false`, so the agent
retries mid-turn — burning latency/tokens — and can **never** search
rich-text body content (note bodies, `about`, etc.).
## Fix
Give `RICH_TEXT` its own case in the filter-schema generator that
exposes the `markdown` and `blocknote` sub-fields, each carrying the
text operators — mirroring the existing composite patterns for `EMAILS`
(`primaryEmail`), `PHONES` (`primaryPhoneNumber`), `LINKS`
(`primaryLinkUrl`), `FULL_NAME`, and `ADDRESS`.
So the agent now emits:
```jsonc
{ "noteBody": { "markdown": { "ilike": "%onboarding%" } } } // valid composite sub-field filter
```
instead of:
```jsonc
{ "noteBody": { "ilike": "%onboarding%" } } // throws on composite root
```
This both **stops the throw** and **makes rich-text content actually
searchable** (the original intent). `TEXT` keeps its existing root-level
scalar operators unchanged.
## Test
Added `__tests__/field-filters.zod-schema.spec.ts`:
- `RICH_TEXT` routes pattern operators onto `markdown` / `blocknote`
- root-level scalar operators on `RICH_TEXT` are no longer accepted
- `TEXT` root-level operators unchanged
## Notes
- No DB/schema migration; render/tool-schema layer only.
- Reproduced against `twentycrm/twenty:latest`; the faulting code is
unchanged on `main` as of this PR.
---------
Co-authored-by: Rich Roberts <rich.roberts@talentpipe.ai>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
|
||
|
|
0ac4f237c0 |
fix(server): stop redundant lambda rebuilds causing build-lock acquisition failures (#21442)
## Context `Lambda invocation failed for function '<id>' during build: Failed to acquire lock for key: lambda-build:<id>` fires ~1000 times/day in production. ## Root cause `LambdaExecutorManagerService.buildExecutor` re-checks `canSkip` inside the `lambda-build:<functionId>` lock, but the re-check reuses the `flatApplication` snapshot captured when the request started. `canSkip` depends on `!flatApplication.isSdkLayerStale`, so: 1. An app sync/install regenerates the SDK client and sets `isSdkLayerStale = true` 2. All in-flight executions of the function fail `canSkip` and queue on the lock 3. The first holder rebuilds and `markSdkLayerFresh` clears the flag in DB + workspace cache 4. Queued waiters can't see that fix — their in-memory snapshot still says stale — so **each waiter redoes the full rebuild serially** (download SDK archive, delete + republish layer, update function config, wait for update) 5. The lock is held back-to-back for minutes; everyone deeper in the queue exhausts the 120s retry budget and throws The local driver already handles this correctly (`LocalLayerManagerService` refreshes the flat application from the workspace cache inside its lock); the lambda driver missed it. ## Fix - Refresh `flatApplication` from the workspace cache inside the lock before re-checking `canSkip`, so waiters skip in ~100ms once the first holder finishes - Degrade gracefully on lock-acquisition timeout: re-check build status with fresh data and proceed with the invocation if the executor is already usable, instead of failing the run (introduces a typed `CacheLockAcquisitionError` so only that case is caught) ## Test plan - [x] `cache-lock.service.spec.ts` passes - [x] `lint:diff-with-main` + typecheck pass - [ ] Monitor `Failed to acquire lock for key: lambda-build:*` error rate in production after deploy |
||
|
|
184c4948d6 |
security: strip Node dev headers from images + lingui 5.9.5 (drops vulnerable esbuild) (#21448)
## Context
AWS Inspector flags the `prod-twenty` image (built from current main)
with 16 findings, and Dependabot alert 174 flags esbuild. This PR fixes
the OpenSSL scanner findings and the esbuild CVE. The typeorm bump
(CVE-2025-60542) was **pulled out of this PR** — see "typeorm status"
below.
## Changes
### Strip `/usr/local/include/node` from runtime stages
(`twenty-server`, `twenty-app-dev`)
15 OpenSSL CVEs (June 9 advisory, incl. CRITICAL CVE-2026-34182) are all
detected via **Node's bundled OpenSSL dev headers**: 3 GENERIC
`openssl/openssl` 3.5.6 detections per CVE at
`/usr/local/include/node/openssl/archs/linux-x86_64/{asm,asm_avx2,no-asm}/include/openssl/opensslv.h`.
The headers are only needed by node-gyp and native addons are compiled
in the build stages — nothing compiles at runtime. Dropping them clears
all 45 detection instances and permanently ends this class of finding
(third occurrence: 3.5.5 → 3.5.6 → 3.5.7). None of these CVEs are
reachable through Node (no CMS/PKCS#7 API, `pfx` is operator-supplied,
Node's QUIC uses ngtcp2, ASN.1 issues need ~2GB inputs).
**Follow-up (~June 17, 2026):** the `node` binary itself still
statically links OpenSSL 3.5.6 — invisible to the scanner after this PR
and unreachable in practice, but the real fix is bumping the pinned
`node:24-alpine` digest once the [announced June 17 Node.js security
releases](https://nodejs.org/en/blog/vulnerability/june-2026-security-releases)
ship a 24.x linking OpenSSL ≥ 3.5.7 (verify via
`deps/openssl/openssl/VERSION.dat` on the release tag — 24.16.0 is still
on 3.5.6). A dated TODO sits next to the cleanup in the Dockerfile.
### esbuild dev-server CORS CVE (Dependabot alert 174,
GHSA-67mh-4wv8-2f99)
`@lingui/cli@5.1.2` (pins `esbuild ^0.21.5`) was the last parent
resolving a vulnerable esbuild (≤ 0.24.2 lets any website send requests
to the dev server and read responses). Instead of a resolution override,
this bumps the lockstepped **lingui suite 5.1.2 → 5.9.5** (within-major;
lingui adopted `esbuild ^0.25.1` in 5.4.1), which:
- removes `esbuild@0.21.5` and all its platform packages from the
lockfile with no forced ranges;
- drops the `@lingui/core` lockstep resolution (its comment marked it
droppable on the next coordinated lingui bump — the tree now resolves a
single `@lingui/core@5.9.5`);
- `@lingui/swc-plugin` stays at `^5.11.0` (peers on `@lingui/core: 5`;
its 6.x line targets lingui 6).
**lingui 5.9.5 behavioral fallout handled here:**
- Translation functions now **throw without an active locale** (5.1.2
fell back silently). The global `i18n` singleton that backs server-side
`` t`…` `` calls only had a messages compiler set, never an activated
locale → activate the source locale in `I18nService.loadTranslations()`,
mirrored in the server jest setup (unit tests bypass Nest bootstrap).
- `msg`/`t` placeholders are now strictly typed (reject
`null`/`undefined`/`unknown`) → one server call site and 16 twenty-front
files adapted with minimal nullish-coalescing fixes that preserve
rendering.
- `.po`/compiled-catalog churn from the new extractor/compiler
(reference reordering, sorted keys — verified content-identical on
unchanged `.po` inputs) is intentionally not committed: the scheduled
i18n workflows regenerate those.
## typeorm status (pulled out)
typeorm 0.3.20 → 0.3.26 was originally in this PR but **made workspace
metadata sync intermittently lossy**: `example-app-postcard` failed
twice with a *different* field missing from the synced PostCard object
each run, and one integration shard's `DataSeedWorkspaceCommand` died
with "Could not find flat entity with universal identifier …" — versus
zero such failures on recent main. Local runs (db reset + seed, group-by
integration suite 19/19) pass, so it is a nondeterministic
CI-load-sensitive regression that needs dedicated debugging (typeorm
changed LIMIT/OFFSET 0 semantics, lazy count for `getManyAndCount`,
upsert WHERE construction, and topological-sort internals in that
range). The resolutions comment documents this as the blocker;
CVE-2025-60542 is MySQL-driver-only (`sqlstring`), so Postgres-only
Twenty is not exposed in the meantime.
## Verification
- `npx nx typecheck twenty-server` / `twenty-front` — clean (no cache)
- `npx nx test twenty-server` — full suite green
- `lingui:extract` + `lingui:compile` — clean for twenty-server /
twenty-emails / twenty-front
- `oxfmt --check` — clean for both packages
- Lockfile diff: lingui 5.9.5 entries, `esbuild@0.21.5` +
`@esbuild/*@0.21.5` platform packages removed, no typeorm changes
|
||
|
|
303c415dd1 |
fix(ai) - add logs + remove dashboard building (#21440)
- add logs for thread finishing without agent message - add logs to monitor toolCall token usage - remove dashboard building via AI (before fixing it) - fix Anthropic compute |
||
|
|
a6fcbf58e4 |
fix(billing) - enable upgrade if invoice already paid (#21450)
Had an issue concerning a user with credits, then invoice automatically paid. Upgrade failed |
||
|
|
462dd3b0e9 |
security: uuid CVE — bump bullmq/msal/blocknote + scoped resolutions for the rest (Dependabot alert 1289) (#21441)
Closes the uuid Dependabot alert — [1289](https://github.com/twentyhq/twenty/security/dependabot/1289) — by **upgrading the parents that bump cleanly** and **scope-resolving only the ones that genuinely can't**. `uuid < 11.1.1` (buffer-bounds check in v3/v5/v6) is pulled by ~9 transitives. ### Bumped (parent upgrade — drops uuid<11, no behavior change; typecheck verified) - **bullmq** 5.40.0 → 5.78.0 — also aligned **ioredis** 5.6.0 → 5.10.1 (bullmq pins it) and fixed the renamed `Job.returnValue→returnvalue` / `stackTrace→stacktrace` (now `string[]|null`) in `admin-panel-queue.service.ts`. - **@azure/msal-node** ^3.8.4 → ^5.2.3 (5.2.4 was age-gate-quarantined). - **@blocknote/** ×5 ^0.47.3 → ^0.51.4. ### Scope-resolved to uuid 11.1.1 (no clean bump exists) - **sockjs** (latest; pinned by webpack-dev-server) and **@ptc-org/nestjs-query-typeorm** (9.4.0 *is* latest, pins `^10`) — no version drops uuid. - **typeorm** — a `patch:` dep / ORM core, too risky to bump. - **node-ical** 0.26 (type-model overhaul → caldav-parser rewrite) and **googleapis** 173 (Gmail/OAuth, 105→173) — large breaking migrations; **deferred to dedicated PRs**. - **@cypress/request** — transitive (cypress isn't a direct dep). Resolutions are **per-package** and preserve the intentional **uuid 13.x** (twenty-sdk / create-twenty-app). ### Verification - `twenty-server` typecheck ✓ (0 errors), `twenty-front` typecheck ✓ (0 errors). - `yarn install --immutable` ✓; every uuid resolves to **11.1.1** or **13.0.2**. - bullmq/msal/typeorm runtime exercised by the **server integration tests**; @blocknote by the **storybook tests** in CI. |
||
|
|
233a6f9fb1 |
fix(server): register Lingui message compiler to stop "Uncompiled message detected" log flood (#21416)
## Summary Fixes #21415. Server-side `` t`…` `` macro calls (e.g. `FlatEntityMapsException`) resolve against Lingui's global `i18n` singleton, which `I18nService` never loads a catalog into and never registers a messages compiler on. With no `_messageCompiler` set, every such lookup logs `Uncompiled message detected!` and falls back to the raw string — flooding server logs (enough to hit hosting log rate limits) and disabling ICU interpolation on those messages. The warning is emitted unconditionally by `@lingui/core` (not `NODE_ENV`-gated). ## Changes Register `compileMessage` via `setMessagesCompiler` in `I18nService.loadTranslations()`: - on the global `i18n` singleton (used by the `t` macro), and - on each per-locale instance. `@lingui/message-utils` is already a transitive **runtime** dependency of `@lingui/core` (`@lingui/core@5.1.2 → @lingui/message-utils@^5.1.2`), so no new dependency is added. ## Testing Deployed on a real instance: before, the server emitted hundreds of `Uncompiled message detected` lines/sec (saturating the log rate limit); after, steady-state shows `0`, and the worker (which shares the code path) likewise shows `0`. App behaviour is unchanged — messages already rendered via fallback; this silences the warning and enables ICU on the affected messages. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com> |
||
|
|
20c83e1f86 |
fix(kanban): preserve scroll on board re-init + propagate same-column reorders via SSE (#20637)
closes https://discord.com/channels/1130383047699738754/1504130730840821860 https://github.com/user-attachments/assets/d5833031-01c6-4e46-b699-c29c42435a53 ## Summary Fixes two related issues with the kanban (board view) collaboration experience: 1. **Scroll-to-top on every data change** — `triggerRecordBoardInitialQuery` always scrolled the board to the top, even when re-initializing for a single-record data change (SSE echo of your own mutation, a collaborator's update). Scroll reset only makes sense when the dataset itself changes (filter / sort / group). 2. **Same-column reorders by other users did not propagate** — the server's diff function stripped `FieldMetadataType.POSITION`, so position-only updates produced empty `updatedFields` and short-circuited event emission entirely. SSE clients never received them. ## What's in here - **Frontend** — `useTriggerRecordBoardInitialQuery` now exposes a `triggerRecordBoardInitialQueryWithoutScrollReset` variant; data-driven re-inits in `RecordBoardDataChangedEffect` use it, while genuine filter / sort / group changes keep the scroll-resetting `triggerRecordBoardInitialQuery`. `getRecordBoardEffectsForUpdateInputs` classifies each update as `trigger-initial-query` / `reposition-records` / `none`. For position- or group-only changes we skip the re-query and reposition records in place in the store (`useRepositionRecordsOnBoard`), which avoids the flicker and preserves scroll. - **Server** — removes `POSITION` from `objectRecordChangedValues`' strip list, so position-only updates emit a non-empty diff and flow through SSE. Position is now treated as a field like any other across all event consumers (SSE, webhooks, workflows, logic functions); a trigger with an explicit field filter still excludes it. |
||
|
|
6e147a548b |
security: clear twenty-apps & seed-dependencies CVE alerts (#21410)
Clears the Oneleet/dependency CVE alerts from the `twenty-apps` example/internal app lockfiles and the application-package `seed-dependencies` template — all via parent/direct dependency upgrades, **no `resolutions` overrides**. ## Lock refresh (non-breaking, within existing ranges) - **postcss** 8.5.8/8.5.9 → 8.5.15 — CVE-2026-41305 — postcard, hello-world, self-hosting - **ip-address** 10.1.0 → 10.2.0 — CVE-2026-42338 — postcard, hello-world, self-hosting, twenty-for-twenty - **yaml** 1.10.2 → 1.10.3 — CVE-2026-33532 — call-recording ## seed-dependencies (direct/parent bumps) - **uuid** `^10.0.0 → ^11.1.1` (direct) — CVE-2026-41907 - **body-parser** `^1.20.4 → ^1.20.5`, which pulls **qs** 6.15.2 — CVE-2026-8723 - **socks** 2.8.3 → 2.8.9 (refresh), which pulls **ip-address** 10.2.0 — CVE-2026-42338 ## twenty-for-twenty - **resend** 6.12.0 → 6.12.4 (refresh): 6.12.4 drops the `svix` dep that pulled the vulnerable **uuid** 10.0.0, leaving only uuid 13.0.2 — CVE-2026-41907 All flagged packages were transitive (except the direct seed-deps `uuid`); no app source changes. |
||
|
|
a825dcf2cc |
security: clear 8 Dependabot alerts via transitive/parent bumps (no resolutions) (#21409)
Clears 8 Dependabot alerts via in-range transitive/parent bumps and one dead-dependency removal. **No `resolutions` overrides** were used — every fix is a real version bump within existing semver ranges or a parent upgrade. ### Root `yarn.lock` - **react-router** 6.30.3 → 6.30.4 (open redirect via protocol-relative URL) — pulled through react-router-dom, ranges unchanged — alert #1382 - **yaml** 2.8.1 → 2.9.0 (stack overflow on deeply nested collections) — alert #734 - **uuid** `^13.0.0` → 13.0.2 in twenty-sdk + create-twenty-app (buffer bounds check) — alert #1164 - **ip-address** `^9.0.5` dropped by bumping **socks** 2.8.3 → 2.8.9 (now depends on `ip-address ^10.1.1`, which is unaffected) — alert #1171 ### `seed-dependencies` lockfile - **uuid** `^10.0.0` → `^11.1.1` (direct dep; removed now-redundant `@types/uuid` since uuid v11 ships its own types) — alert #1287 - **ip-address** `^9.0.5` dropped via the same socks bump — alert #1170 ### `twenty-for-twenty` lockfile - **resend** bumped to 6.12.4 (`^6.12.0` range kept), which drops its `svix@1.90.0 → uuid@^10` transitive chain — alert #1278 ### `twenty-companion` - Removed the unused **simplemde** dependency. The note editor loads SimpleMDE from a CDN `<script>` tag and never imports the npm package; `easymde` (its maintained fork) is already a dependency — alert #690 ### Not addressed here The remaining alerts can't be closed without `resolutions` overrides (deliberately avoided in this PR) or a larger migration: - **qs** (#1305, #1304), **lodash** (#824 high / #823 / #385), **ws** (#1238), **postcss** (#1061) — vulnerable copies are pinned exact / bundled by parents (express, body-parser, @nestjs/*, next, styled-components, zapier) with no in-range patch. - **webpack-dev-server** (#1237/#692/#691) — pinned by `@electron-forge/plugin-webpack` (still on v4); dev-tooling only. - **uuid <11.1.1** (#1289) — spread across `^3`/`^8`/`^9` transitive ranges; reaching v11 is a breaking jump. - **apollo-server-core** (#735/#736) — requires an Apollo Server 3 → 4 migration. |
||
|
|
2514cab860 |
fix(server): include relation join column names in updatedFields of update events (#21405)
## Context Since #21052, update-event diffs are keyed by the relation field name (e.g. `company`) instead of the join column name (e.g. `companyId`). `updatedFields` is derived from the diff keys, so any **workflow database-event trigger** (or webhook) configured with a field filter on a relation join column **silently stopped firing** — no run is created at all. We hit this in production: a `cloudWorkspace.updated` trigger filtered on `twentyContactId` stopped creating runs the same day #21052 was deployed. Updating the record's relation produced `updatedFields: ["twentyContact"]`, which no longer matches the stored settings `fields: ["twentyContactId"]` in `WorkflowDatabaseEventTriggerListener.shouldTriggerJob`. ## Solution Keep the diff keyed by relation field name (the timeline rendering from #21052 relies on it — adding both keys to the diff would display relation changes twice), but expose **both** the relation field name and its join column name in `updatedFields`: - New `computeUpdatedFieldsFromDiff()` in `object-record-changed-values.ts`: expands MANY_TO_ONE relation diff keys with their join column name. - Used in `formatTwentyOrmEventToDatabaseBatchEvent` for UPDATED/DELETED/RESTORED and UPSERTED events instead of `Object.keys(diff)`. This restores matching for pre-existing trigger/webhook configurations (join column names) while keeping configurations using relation field names working. ## Test plan - [x] Unit tests: relation diff keyed by relation name; `updatedFields` contains both `company` and `companyId` - [x] End-to-end util test on UPDATED event: `updatedFields: ['company', 'companyId']`, diff keyed by `company` - [x] Downstream consumer specs pass (workflow trigger listener, webhooks, subscriptions, logic-function triggers) - [x] Verified against the production workspace that a `twentyContactId` update currently produces no workflow run with the old behavior |
||
|
|
7258722754 |
security: upgrade @nestjs/graphql 12→13 + @ptc-org/nestjs-query 4→9 (+ @nestjs/config 4) (#21402)
## What Upgrades the NestJS GraphQL stack to clear the High **`ws`** alert (GHSA-3h5v-q93c-6h6q) and modernize off two heavily-patched majors. `@nestjs/graphql@13` pulls `ws@8.20.1` (was 8.16.0). This had to be a **coordinated** upgrade: `@ptc-org/nestjs-query@4.2.0` doesn't support `@nestjs/graphql@13`, so all three move together. | Package | From → To | |---|---| | `@nestjs/config` | 3.3.0 → ^4.0.4 | | `@nestjs/graphql` | 12.1.1 → ^13.4.2 | | `@ptc-org/nestjs-query-{core,graphql,typeorm}` | 4.x → ^9.4.0 | ## The tricky bits - **Re-ported the custom `@nestjs/graphql` patch onto v13.** v13 rewrote the schema builder and added its *own* native multi-schema support (`includeModules`, native `clear()`). Twenty's patch (`resolverSchemaScope` + `computeReachableTypes` — the core/metadata/admin split) is re-merged into v13's new `generate(options, includeModules, reachableTypes)` flow, with a link-preserving `storage.clear()` so cross-schema `resolveType` closures keep working. - **Re-ported the `@ptc-org` patch onto 9.4.0**: removes the `@shareable` federation directive from built-in connection/response types, **and** adds a `.js` extension to its extensionless deep import of `@nestjs/graphql` internals — which v13's new `"exports"` map otherwise rejects at runtime (this was the boot blocker). - **`AppTokenService`**: nestjs-query 9 requires custom services to inject their repo and `super(repo)` it (added an `@InjectRepository` constructor). - **`gridPosition` input fields**: dropped the `deprecationReason` (a *required* input field can't be `@deprecated` under the upgraded graphql) — fields keep their original nullability, so the **schema is unchanged**. - **Service specs**: nestjs-query 9's `TypeOrmQueryService` reads the repo's driver/metadata at construction, so the mocked repos now include `manager`/`metadata`. ## Verification - `nx typecheck twenty-server`: **0 errors**; lint clean - Server boots; **all 3 GraphQL schemas** (`/graphql`, `/metadata`, `/admin-panel`) generate and respond `200` - `graphql:generate` for all 3 schemas is **byte-identical** to before the upgrade (the reachable-types re-port is faithful) - **108 service unit tests pass** (incl. all 6 `TypeOrmQueryService` services) - `ws@8.16.0` gone (now 8.17.1 + 8.18.0); `yarn install --immutable` clean ## Note on lodash `lodash@4.17.21` still remains via `zapier-platform-core` (runtime) and `@stoplight/spectral`, so the lodash alert is **reduced but not fully cleared** by this PR — it needs those separate sources addressed (or a resolution). |
||
|
|
3655942fa4 |
fix: reload stale clients on any older app version, not just major (#21011)
## Context
The GraphQL error handler emits an `APP_VERSION_MISMATCH` error
(surfaced on the client as a "your app version is out of date, please
refresh" prompt) when a client's app version is behind the server's.
Today that comparison only fires when the **major** version differs:
```ts
const frontEndMajor = semver.parse(frontEndAppVersion)?.major;
const backendMajor = semver.parse(backendAppVersion)?.major;
if (isDefined(frontEndMajor) && isDefined(backendMajor) && frontEndMajor < backendMajor) { ... }
```
Twenty ships schema changes in minor and patch releases too. A tab left
open across a minor/patch deploy keeps sending GraphQL documents built
against the previous schema. If a field was removed or renamed, those
operations fail with opaque field-level errors and the user never gets
the refresh prompt — a hard reload is the only recovery.
## What this does
- **Server:** fires the mismatch whenever the client version is strictly
older than the server (`semver.lt`), regardless of which version
component changed.
- **Client:** `onAppVersionMismatch` now reloads the page once (in
addition to the existing snackbar) to pull the current `index.html` and
hashed assets, guarded by a short `sessionStorage` window so a
still-stale reload can't cause a refresh loop.
## Notes
- No behavior change for clients on the same or newer version.
- Relies on the existing `x-app-version` header and `APP_VERSION` config
that already drive this check.
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
ce2d77be2a |
feat(server): in-app server-level admin management (#19785) (#21321)
## Closes #19785 In-app management of **server-level admin rights** (`canAccessFullAdminPanel`, `canImpersonate`) so self-hosters no longer need raw SQL + a Redis flush + restart to grant access. > **Draft** — feature complete; `/code-review` + `/security-review` run and addressed. ### Background `AdminPanelGuard` / `ServerLevelImpersonateGuard` read `request.user.{canAccessFullAdminPanel,canImpersonate}`, hydrated each request from `CoreEntityCacheService.get('user', …)` (local 30-min + Redis no-TTL). The cache was only invalidated on soft-delete, so a raw `UPDATE core."user"` never took effect. The **first** signup auto-gets both flags; every subsequent admin previously needed raw SQL. ### UX - **Admin Panel → General → Administrators**: a read-only overview of every user with server-level access; each row links to that user's admin page. - **Find anyone** via the user search (Recent Users) — available to full admins and impersonators — then open their **admin user page**. - On the user page, an **"Administrator access"** card (gated on `canAccessFullAdminPanel`) has two toggles — *Full admin panel access* and *Impersonation* — that work for **any** user (a user with no access shows both off). Mirrors how **Impersonate** already works (find user → user page → act). Each change opens a confirm dialog with a **2FA code** field; the last full admin's toggle is disabled. ### Backend / security - **Cache fix** — invalidate the user entity cache on committed user updates (not just soft-delete) so privilege changes propagate (~100 ms, cluster-wide) with no restart. - `getServerAdmins` query + `updateServerAdminAccess` mutation (any `targetUserId`), gated on `canAccessFullAdminPanel`. - `NoImpersonationGuard` on both — an impersonated full-admin session can't be used to escalate an impersonator. - Fresh **2FA TOTP step-up** (enrolled+verified method **and** a fresh code; genuine 2FA errors surface; dev-skip on trusted `NODE_ENV`). - **Last-admin lockout** in a transaction with a pessimistic row lock (no TOCTOU). - **Email-to-all-admins + affected user** (rendered once per locale), structured log, audit event-log emit. - **Authorization**: the read-only `userLookupAdminPanel` + `adminPanelRecentUsers` lookups now accept `canAccessFullAdminPanel OR canImpersonate` (new `AdminPanelOrImpersonateGuard`), so a full admin without impersonate can still find users to manage. Workspace/impersonation queries stay impersonate-gated. ### Reviews - `/code-review` (max effort): 3 security findings (impersonation-escalation sink, lockout TOCTOU, step-up accepting PENDING 2FA) — **all fixed**. `/simplify`: applied. `/security-review`: **no high/medium vulnerabilities**. ### Follow-ups (not in this PR) - Unit tests for `AdminPanelServerAdminService` + a frontend test. - Point the self-host troubleshooting docs at the new UI. - OTP retry UX: `ConfirmationModal` closes on confirm, so a wrong code needs a reopen (kept to reuse the existing modal; no new pattern). ### Notes for reviewers - `generated-admin/graphql.ts` entries were hand-added to match codegen output (admin codegen needs a running server); re-run `nx graphql:generate twenty-front --configuration=admin` to confirm parity. - First-admin bootstrap (first signup) is unchanged. --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
06d68f665d |
fix(auth): additional workspace and identity validation in auth flows (#21347)
## What Adds validation across three auth flows so that a session or reset link is consistently scoped to a workspace the authenticated principal belongs to, and to a verified identity. - **Access token** (`jwt.auth.strategy.ts`): when resolving the request's user context, the token's `userWorkspaceId` must belong to the token's `workspaceId` — the same check the application-token path already performs. - **OIDC** (`oidc.auth.strategy.ts`): reject sign-in when the identity provider explicitly reports `email_verified: false`. - **Password reset** (`reset-password.service.ts`): a supplied `workspaceId` is only used when the user is a member of it; otherwise it falls back to the user's own first password-auth-enabled workspace. ## Tests - `jwt.auth.strategy.spec.ts`: rejects an access token whose user workspace belongs to a different workspace than the token; existing mocks updated to carry the cached `workspaceId`. - `oidc.auth.strategy.spec.ts` (new): rejects unverified email; accepts verified and absent-claim cases. - `reset-password.service.spec.ts`: falls back when the supplied `workspaceId` is not one the user belongs to. `tsgo`, `oxlint` and `oxfmt` all clean on the changed files. |
||
|
|
cf70565976 |
feat(twenty-server): allow shouldHideEmptyGroups in app view manifest (#21370)
## Context The view **Hide empty groups** setting (`shouldHideEmptyGroups`) can be toggled in the UI, is persisted on the `View` entity, exposed in the `CreateView`/`UpdateView` GraphQL inputs, and tracked by the flat-view sync machinery — but it could **not** be set from an app's view manifest. Root cause: the field postdates the manifest plumbing (added in #16385, Dec 2025). Two spots were never updated to thread it through: - `ViewManifest` didn't declare the field. - `fromViewManifestToUniversalFlatView` hardcoded `shouldHideEmptyGroups: false`. Ref: twentyhq/core-team-issues#414 ## Changes - Add optional `shouldHideEmptyGroups?: boolean` to `ViewManifest`. - Read it in the converter (`?? false`), mirroring the existing `isCompact` handling. - Cover it in the converter unit test (default + explicit value). No migration or schema change — the column already exists, and downstream sync (`FLAT_VIEW_EDITABLE_PROPERTIES` + the universal-flat compare type) already handles it. ## Test - `npx jest from-view-manifest-to-universal-flat-view` → 5 passed - `tsgo -p tsconfig.json` (twenty-server) → no new errors - oxlint + oxfmt clean |
||
|
|
9c66975520 |
isCustom deprecation for Objects and Fields (#21228)
## Context
`isCustom` was a legacy denormalized boolean on `ObjectMetadataEntity`
and `FieldMetadataEntity`.
Now that every metadata row carries `applicationId` (via
`SyncableEntity`), "is this custom" is fully derivable, and the stored
boolean was a redundant second source of truth that could drift.
The real meaning of `isCustom` is **"the owning application is not the
twenty-standard application"** — i.e. `!belongsToTwentyStandardApp`.
Note this is *not* "belongs to the workspace custom app" as I initially
thought: third-party-application
objects/fields are custom too.
The standard application has a globally stable `universalIdentifier`, so
the value derives with no per-workspace lookup.
## Changed
## `isCustom` checks — before → after
`isCustom` is no longer a stored column. The table below lists every
site that branched on it and how it resolves now. The unifying rule:
`isCustom ≡
!isTwentyStandardApplicationUniversalIdentifier(applicationUniversalIdentifier)`.
### Server — behavioural checks
| Location | Purpose | Before | Now |
|---|---|---|---|
| `utils/compute-object-target-table.util.ts` | Physical table name `_`
prefix | `computeTableName(nameSingular, objectMetadata.isCustom)` |
derives from `applicationUniversalIdentifier` (single source for all
table-name callers) |
| `twenty-orm/factories/entity-schema.factory.ts` +
`…/entity-schema-metadata.type.ts` | ORM table name (hot path) |
`object.isCustom` | `object.applicationId !== standardApplicationId`
(computed in `buildEntitySchemaMetadataMaps`) |
|
`twenty-orm/repository/workspace-{delete,soft-delete,update}-query-builder.ts`
| Table name for mutations | `computeTableName(nameSingular,
objectMetadata.isCustom)` | `computeObjectTargetTable(objectMetadata)` |
| `index-metadata/utils/generate-deterministic-index-name-v2.ts` | Index
name hash (must stay bit-identical) | `flatObjectMetadata.isCustom` |
derives from `applicationUniversalIdentifier` |
| `object-metadata/object-record-count.service.ts` | Table name for
record count | `computeTableName(nameSingular, isCustom)` |
`computeObjectTargetTable(flatObjectMetadata)` |
|
`workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts`
| Match seed config by table name | `computeTableName(item.nameSingular,
item.isCustom)` | `computeObjectTargetTable(item)` |
| `commands/workspace-export/workspace-export.service.ts` +
`…/utils/generate-workspace-schema-ddl.util.ts` | Export table name (raw
entity) | `objectMetadata.isCustom` |
`!isTwentyStandard…(objectMetadata.application?.universalIdentifier)` |
|
`flat-field-metadata/services/flat-field-metadata-type-validator.service.ts`
| Block users creating reserved field types |
`args.flatEntityToValidate.isCustom` |
`!args.flatEntityToValidate.isSystem` |
| `api/common/.../common-create-many-query-runner.service.ts` | Don't
let client overwrite system `createdBy` |
`createdByFieldMetadata.isCustom === false` |
`createdByFieldMetadata.isSystem === true` |
|
`field-metadata/utils/resolve-field-metadata-standard-override.util.ts`
| Skip i18n/overrides for custom fields | `if (fieldMetadata.isCustom)
return raw` | **removed** — falls through on
`isDefined(standardOverrides)` |
|
`object-metadata/utils/resolve-object-metadata-standard-override.util.ts`
| Skip i18n/overrides for custom objects | `if (objectMetadata.isCustom)
return raw` | **removed** — same fall-through |
|
`command-menu-item/utils/build-navigation-interpolation-context.util.ts`
| Override context for nav labels | passed `isCustom` into resolver |
dropped (resolver no longer needs it) |
| `api/common/.../data-arg-processor.service.ts` | `isCustom` for
record-position table name | `flatObjectMetadata.isCustom` | derives
from `applicationUniversalIdentifier` |
| `metadata-modules/minimal-metadata/minimal-metadata.service.ts` |
Minimal DTO + override context | `flatObjectMetadata.isCustom` | derives
from `applicationUniversalIdentifier` |
|
`commands/upgrade-version-command/1-23/…backfill-record-page-layouts.command.ts`
| Filter to custom objects | `objectMetadata.isCustom` |
`!isTwentyStandard…(applicationUniversalIdentifier)` |
### Server — DTO / API population
| Location | Before | Now |
|---|---|---|
|
`flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util.ts`
| passthrough `isCustom` | derives from `applicationUniversalIdentifier`
|
|
`flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util.ts`
| passthrough `isCustom` | derives from `applicationUniversalIdentifier`
|
|
`object-metadata/utils/from-object-metadata-entity-to-object-metadata-dto.util.ts`
(REST) | `entity.isCustom` | `entity.applicationId !==
standardApplicationId` |
|
`field-metadata/utils/from-field-metadata-entity-to-field-metadata-dto.util.ts`
(REST) | `entity.isCustom` | `entity.applicationId !==
standardApplicationId` |
| `dataloaders/dataloader.service.ts` | passed
`flatFieldMetadata.isCustom` into override resolver | dropped (resolver
no longer needs it) |
> REST controllers (`object-metadata.controller.ts`,
`field-metadata.controller.ts`) resolve `standardApplicationId` once per
request from the cached `flatApplicationMaps`.
### Frontend
| Location | Purpose | Before | Now |
|---|---|---|---|
| `settings/.../SettingsObjectFieldDisabledActionDropdown.tsx` | Whether
an inactive field is deletable | `isDeletable = isCustomField` |
`isDeletable = isCustomField && !isSystemField` |
### Unchanged (out of scope)
`isCustom` on `IndexMetadata` / `View` / `Skill` / `Agent` and their
guards still read the persisted column.
Breaking change is on the isCustom filter on field and object APIs, this
is never used in the FE and unlikely used by external consumers
|
||
|
|
7530775dc1 |
fix(billing) - Suspend workspace at trial period end if cancelation is planned (#21363)
https://twenty-v7.sentry.io/issues/6900026484/events/e5a9e11fe0464df296d8beeabcf0f538/?end=2026-06-09T05%3A24%3A00&project=4507072499810304&query=%21twenty.workspace.id%3A%EF%80%8DContains%EF%80%8Dad6668da-9f4c-4618-9424-9092ea87db26%20%21twenty.workspace.id%3A%EF%80%8DContains%EF%80%8D86b352c5-ffff-4a4c-8660-b1b2bfb7f57d&referrer=next-event&start=2026-05-23T06%3A02%3A00 |
||
|
|
c27c8c88b0 |
Fix various graphs bugs (#21311)
Some bugs fixed in this PR
1. From UI any field could be chosen to group the query by it, while for
instance, RAW_JSON type (eg workflowRun.state) is not supported by
PostgreSQL to group a query by. Fix: removed it from the "group by"
fields options in FE + in BE -->
2. The BE check existed (isFlatFieldMetadataSupportedInGroupBy) but the
signature was malformed: it expected`{ fieldMetadataType,
fieldMetadataName, fieldMetadataIsSystem }` while every caller passes a
flat field metadata object with type/name/isSystem. So the check is
mis-wired — at runtime the destructured props are undefined, making it
always return true (validation bypassed). Fixed this.
3. Group by does not work with Morph relations if their direction is
ONE_TO_MANY. Added that constraint.
4. Group by with morph relations were broken even for MANY_TO_ONE,
because a morph is stored as one field per target
(polymorphicOwnerRocket, polymorphicOwnerSurveyResult…), each with its
own join column, but the frontend collapsed them into a single
polymorphicOwner field — so the backend tried to resolve a non-existent
polymorphicOwnerId. Fix: Frontend: added a target picker so you choose
the specific morph target (then its sub-field), storing the real
per-target field id. Backend: fixed validate-relation-subfield to use
the per-target field's own relationTargetObjectMetadataId instead of the
multi-target resolver that returned null.
5. (improvement) When an error occured in the query, the graph showed
"No data". Updated it to "error". (screenshot 1)
6. When a field used as a filter on a graph is deleted, it is not
deleted as a graph filter (which is ok because it would involve parsing
all the graph's configuration json to find whether a field is
referenced; there is no foreign key), which prevented from further
modifying the graph's filters. Fixed this + add an indicator that the
filter is can/should be removed (see screenshot 2)
7. "Ambiguous column name" PG error occurs when ordering by "creation
date" of a related field, because both objects have createdAt field.
Fixed it by adding table alias as prefix.
8. (improvement) While working on #5 I did not understand why we could
directly do `"objectMetadataNameSingular"."columnName" `while I expected
that for custom objects it would have to be
`_objectMetadataNameSingular`. that's simply because we use an alias
from the beginning. To add clarity, within groupBy code I replaced
`objectMetadataNameSingular` with `objectAlias` everywhere it is indeed
inherited from us using objectAlias.
<img width="685" height="391" alt="Screenshot 2026-06-08 at 12 01 45"
src="https://github.com/user-attachments/assets/f2b15ca5-da39-4114-8188-69f58f3c4cbf"
/>
<img width="598" height="341" alt="Screenshot 2026-06-08 at 11 53 55"
src="https://github.com/user-attachments/assets/66372811-4a37-40d9-b43a-4af51f89b6e6"
/>
|
||
|
|
137fe45cf6 |
Deprecate dummy enterprise key 2/2 (#21328)
Following [1/2](https://github.com/twentyhq/twenty/pull/20890) Now that all usages of hasValidEnterpriseKey has been removed in prod and deployed, we can safely remove it altogether. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
fcaf2b4d9b |
chore(twenty-server): temporary instrumentation for app-install 504 (#21365)
## Why App installs on cloud intermittently fail with a 504, surfacing in Sentry as `Migration action 'update' for 'logicFunction' failed` + `Failed to rollback transaction: Query runner already released`. This is **temporary instrumentation** to pin down where the time goes — to be reverted once the bottleneck is fixed. Everything is greppable via `[install-perf]` and marked `// TODO(install-perf)`. ## What the local repro already told us I instrumented the manifest-sync/migration path and ran a local harness (new skipped spec) installing **1 / 8 / 30 logic functions**, for both create and the checksum-bump **update** (the incident path): | stage (N=30, update) | ms | |---|---| | flat-maps recompute | ~1 | | build migration | ~11 | | transaction (all actions + commit) | ~79 | | post-commit cache invalidate | ~6 | | **full sync** | **~135** | Nothing approached 1s, let alone 10s; no slow queries logged. So the migration/cache code is **not** the algorithmic cause. Given the in-transaction `UPDATE ... WHERE id=?` is intrinsically fast, a >10s in prod almost certainly means it was **blocked on a lock**, and the 10s node-pg `query_timeout` (`core.datasource.ts`) then killed the connection → the observed errors + 504. Local can't reproduce prod lock contention / table sizes, hence this instrumentation. ## What this adds (all `TODO`-marked) - **hrtime per-stage timing** — flat-maps recompute, build vs run, per-action (`>50ms`), transaction summary, post-commit cache invalidation. Uses `process.hrtime` because the integration harness enables fake timers (so `Date.now()` is useless there). - **`maxQueryExecutionTime`** slow-query logging on the core datasource (logs the offending SQL). - **Scoped `SET LOCAL lock_timeout = '8s'`** on the migration transaction (below the 10s `query_timeout`) → a blocked action fails fast with a clear *"canceling statement due to lock timeout"* instead of the opaque connection kill. - **Best-effort `pg_stat_activity` snapshot on failure** (on a fresh pooled connection) to identify the blocking session, plus a **guarded rollback** so a released connection stops masking the real error. - **Skipped local perf harness** (`logic-function-install-performance.integration-spec.ts`) — run manually with `nx test:integration:with-db-reset -- --testPathPattern "logic-function-install-performance"`. ## How we'll use it Deploy, reproduce the failing install, and read the `[install-perf]` logs: the per-action timing names the action, the `lock_timeout` message + `pg_stat_activity` snapshot name the **blocking** query/PID. Then revert this PR and fix the actual contention. Typecheck (`nx typecheck twenty-server`) is clean. |
||
|
|
e9086b49f6 |
Increase logicFunctionQueue worker concurrency to 10 (#21364)
## Summary - Increase BullMQ worker concurrency for `logicFunctionQueue` from 1 (default) to 10 - Logic function executions are I/O-bound Lambda calls — the worker just holds an HTTP connection open, so higher concurrency doesn't add CPU/memory pressure - With 13 worker pods, this goes from 13 to ~130 concurrent slots, which should resolve the ~3h average queue latency observed in Grafana ## Test plan - [ ] Monitor `avg_latency_ms` for `logic-function-queue` in the Grafana job queue dashboard after deploy - [ ] Verify worker pod CPU/memory remains stable |
||
|
|
ed7ff4a84b |
chore: bump version to 2.12.0 (#21358)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
20ccc52424 |
fix(auth): align workspace resolution across SSO and OTP sign-in flows (#21346)
## What
Makes workspace resolution consistent across the SSO, OTP and
login-token sign-in flows — the target workspace is derived from the
authenticated principal rather than from separate request-supplied
values.
- **SSO callback** (`sso-auth.controller.ts`): validates the resolved
workspace against the authenticating identity provider's own workspace,
so every SSO session is scoped to the provider that issued it. The
`workspaceInviteHash` is request-controlled and shouldn't select a
different workspace than the provider.
- **`getAuthTokensFromOTP`** (`auth.resolver.ts`): reuses the shared
`validateWorkspaceAccess` helper already used by
`getAuthTokensFromLoginToken`, so the login token and the
origin-resolved workspace are checked the same way in both flows.
- **SSO enablement** (`auth.service.ts`, `workspace.validate.ts`): SSO
sign-in now checks the workspace operates an active SSO identity
provider, matching how the other providers gate on their per-workspace
settings, instead of treating SSO as unconditionally enabled.
## Tests
- `auth.service.spec.ts`: SSO sign-in throws when the workspace has no
active SSO identity provider; proceeds when it does.
The controller and resolver spec additions were dropped from this PR;
the behaviours below cover them via manual testing on `main`.
`tsgo`, `oxlint` and `oxfmt` all clean on the changed files.
## How to test on main
Check out this branch on top of `main` and exercise each flow against a
multi-workspace setup (workspace **A** and workspace **B**, each on its
own domain).
**1. SSO callback is scoped to the issuing provider**
(`sso-auth.controller.ts`)
- Configure an SSO identity provider (SAML or OIDC) on workspace **A**
and set it to **Active**.
- Start an SSO sign-in for workspace **A**, but tamper with the callback
so the resolved workspace points at **B** (e.g. supply a
`workspaceInviteHash` belonging to **B**).
- Expected: the callback is rejected with `OAUTH_ACCESS_DENIED`
("Identity provider does not belong to this workspace"). A clean
callback that resolves to **A** still completes sign-in.
**2. Inactive SSO provider blocks sign-in** (`auth.service.ts` /
`workspace.validate.ts`)
- Take workspace **A**'s SSO identity provider and set its status to
something other than `Active` (e.g. inactive/draft).
- Attempt SSO sign-in for **A**.
- Expected: sign-in is denied with `OAUTH_ACCESS_DENIED` ("Identity
provider not found"). Flipping the provider back to `Active` lets
sign-in proceed.
**3. OTP login token must match the origin workspace**
(`getAuthTokensFromOTP` in `auth.resolver.ts`)
- Enable two-factor authentication for a user who belongs to workspace
**A**.
- Sign in to obtain a login token scoped to **A**, then call
`getAuthTokensFromOTP` (submit the OTP) from workspace **B**'s
origin/domain.
- Expected: the request is rejected with `FORBIDDEN_EXCEPTION` ("Token
is not valid for this workspace") and no tokens are issued. Submitting
the OTP from **A**'s origin issues tokens as before.
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
16db47fb70 |
fix: add queue attribute to jobs waiting gauge metric (#21324)
## Summary
- The `twenty_queue_jobs_waiting_total` gauge was summing all queues
into a single value without a `queue` label, making the Grafana "Jobs
Waiting by Queue" panel show a single aggregated line instead of
per-queue breakdown.
- Uses `getMeter()` directly to call `observableResult.observe(count, {
queue: queueName })` per queue, matching the `by (queue)` grouping the
dashboard already expects.
## Test plan
- [x] Deploy and verify the Grafana "Jobs Waiting by Queue" panel
displays separate series per queue
- [x] Confirm Prometheus scrape returns
`twenty_queue_jobs_waiting_total{queue="..."}` with distinct queue
labels
|
||
|
|
77d1e8ced6 |
feat(app-dev): sync error hints, flatEntity labels, dev-mode summary UI, and docs (#21252)
Split out of #21240 — all remaining app-dev improvements. Stacked on #21251 (review/merge that first). - Actionable recovery hints on failed syncs; unified diff renderer; `--dry-run` guard. - Return `flatEntity` on update/delete sync actions and unify the diff label. - Summarize the dev-mode entity list unless `--verbose`. - Docs: syncing & recovery guide + dry-run + open-an-issue prompt. - Live execution mode for synced logic functions; clearer manifest warnings. <img width="1018" height="700" alt="image" src="https://github.com/user-attachments/assets/5e9ce19e-0f1d-4f99-8524-4e118bde932b" /> |
||
|
|
2151a414f5 | Remove IS_WORKFLOW_RUN_STEP_LOGS_ENABLED feature flag (#21323) | ||
|
|
45ceb5d74c |
security: bump vulnerable deps in logic-function runtime lockfiles (#21317)
## What Two standalone dep-set lockfiles bundled for the logic-function / application-package sandbox carried vulnerable transitive versions. Within-range bumps only (these dep sets are handed to user logic functions, so no breaking majors): **`seed-dependencies`** - `nodemailer` `^8.0.4 → ^8.0.5` (resolves 8.0.10) — SMTP CRLF command injection (direct dep) - `brace-expansion`, `picomatch`, `lodash` → patched **`common-layer-dependencies`** - `lodash`, `brace-expansion` → patched ## Left for follow-up (can't be fixed within-range) - `qs` — pinned transitively by `body-parser`, stays at 6.14.2 (needs body-parser bump) - `ip-address` 9.x → 10.x and `uuid` 10.x → 11.x — major bumps; left out since these are exposed to user functions and would be breaking `axios` here is already on the patched `^1.16.1`. |
||
|
|
dfb3da1f8d |
feat(emailing-domain): add LOG driver for local development (#21286)
## What Adds a `LOG` driver to the emailing-domain feature, selected via a new `EMAILING_DOMAIN_DRIVER` config variable (defaults to `AWS_SES`, so production behavior is unchanged). The LOG driver: - resolves domains to `VERIFIED` instantly (no DNS / SES setup) - logs each `sendEmail` and returns a synthetic `messageId` instead of calling SES It also dev-seeds a pre-verified domain per workspace (`<workspaceId>.dev.twenty.local`) so the feature works out of the box. ## Why The emailing-domain feature currently ships only the AWS SES driver, so the verify → send flow can't be exercised locally (or in CI) without real AWS credentials. This unblocks local development and review of anything built on emailing domains. ## Usage ``` EMAILING_DOMAIN_DRIVER=LOG ``` The seeded `*.dev.twenty.local` domain is already verified; sends are logged (`[log-driver] sendEmail ...`). --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
36567e96ab |
Remove twenty-sdk from package.json before build (#21305)
Follow up of https://github.com/twentyhq/twenty/pull/21179 Removes twenty-sdk from dependencies when building lambda layer |
||
|
|
b56fea69aa |
fix(ai) - optimize metadata CRUD tools (#21235)
Reduces output tokens for all 13 metadata tools by (~49%) based on
production sampling data.
GET tools (field + object metadata)
System fields are now returned as compact {id, name, type} instead of
the full ~20-key payload (opt-in includeFullSystemFields to get full
payload). System objects are similarly compacted to {id, nameSingular,
namePlural}.
Internal fields the agent never uses (searchVector, deletedAt, position,
updatedBy) are excluded entirely.
workspaceId and applicationId are hoisted into a response envelope
instead of being repeated on every record.
Null/default-false properties are stripped from custom field and object
payloads (e.g. options: null, settings: null, isUIReadOnly: false).
CUD tools (create/update/delete)
Create and update field tools now return {id, name, type, label} instead
of the full DTO.
Create and update object tools now return {id, nameSingular,
labelSingular} instead of the full DTO.
Delete tools return {id, success: true} instead of the full DTO of the
deleted entity.
Validation errors are grouped by message — e.g. 10 fields failing the
same check produce one line with all names instead of 10 identical
lines.
Learn schemas (all tools)
UUID pattern regex stripped from JSON schemas (keeps format: "uuid").
$schema and additionalProperties: false stripped from all generated
schemas.
All Zod .describe() annotations and tool descriptions shortened.
Skill & tool description updates:
All references to the removed list_object_metadata_items tool replaced
with get_object_metadata / get_field_metadata across skill instructions,
dashboard tools, view filter/sort tools, and MCP server instructions.
|
||
|
|
186d5b8faa | revert #21177 (#21284) | ||
|
|
91f2f08995 |
feat(server): unify workspace-event ingestion behind one EventSink pipeline (#21197)
## Why The five event-log streams (`workspaceEvent`, `pageview`, `objectEvent`, `usageEvent`, `applicationLog`) each wrote to ClickHouse through their own fire-and-forget writer (`AuditService`, `UsageEventWriterService`, and the `application-logs` driver), with the per-type knowledge (table names, normalization, access rules) spread across several modules. Three of them reimplemented the same ClickHouse insert, and the read side, the live stream, and the producers lived in different modules under two different names. This consolidates them into one `core-modules/event-logs/` subsystem (emit, write, live, read), with the per-type config in a single registry so adding an event type is roughly one file. The base Logs settings tab and free application logs shipped separately in #21180 (merged). This PR adds the unified backend, the registry, and the viewer's live mode and entitlement gating. ## Pipeline ```mermaid flowchart TB subgraph PROD["Producers"] A["auth, billing, impersonation,<br/>webhook, custom-domain"] U["usage listener"] F["logic-function executor (app logs)"] R["record CRUD (entity events)"] end EM["EventLogEmitterService<br/>createContext().insert* / dispatch()"] EQ(["entityEventsToDbQueue<br/>(existing, shared with timeline)"]) CIE["CreateEventLogFromInternalEvent"] SINK["WorkspaceEventSinkService.ingest()"] C1["ClickHouseEventSink"] C2["ConsoleEventSink"] LIVE["EventLogLiveService.publishWatched()<br/>(presence-gated)"] CH[("ClickHouse, 5 tables, async_insert")] CHAN(["WORKSPACE_EVENTS_CHANNEL"]) RS["EventLogsService (registry-driven read)"] LR["EventLogsLiveResolver"] UI["Settings > Logs"] A --> EM U --> EM F --> EM EM -->|direct| SINK R --> EQ --> CIE -->|ingest| SINK SINK --> C1 --> CH SINK --> C2 SINK --> LIVE -.->|if a viewer is watching| CHAN --> LR --> UI CH --> RS --> UI ``` ## What it does - Producers call `EventLogEmitterService.createContext().insert*()`, which builds a typed `WorkspaceEventEnvelope` and writes it through `WorkspaceEventSinkService` to the configured sinks (ClickHouse, Console) plus a presence-gated live fan-out. Record/CRUD events reach the same sink through the existing `entityEventsToDbQueue`. There is no dedicated queue; ClickHouse `async_insert` batches server-side. Writes are best-effort, as on main today. - `EVENT_LOG_TYPES[table]` is the per-type source of truth: the ClickHouse table, the required entitlement, the free-text filter column, and the row-to-GraphQL mapping. Read row shapes derive from the write rows. - Four modules along their dependency boundaries: `EventLogEmitterModule` (producer API), `EventLogIngestionModule` (sink layer), `EventLogLiveModule` (fan-out), and `EventLogsViewerModule` (the entitlement-gated GraphQL read, which is where billing/enterprise/permissions stay so producers stay light). - Logs viewer: per-table columns, filters (text, date, record), live mode, and an upgrade card that points to Billing on Cloud or the Admin Panel on self-hosted. Application logs are free on every plan; the other four require the `AUDIT_LOGS` entitlement (with a `NO_ENTITLEMENT` fallback to the upgrade card). - Renames `AuditService` to `EventLogEmitterService`, and the generic `Monitoring` event to a typed `Impersonation` event (`level` + `action`). - Removes `UsageEventWriterService`, the `application-logs` driver/module, and `AuditService`'s direct inserts. ## Durability Writes are best-effort, the same as main today (the old writers were fire-and-forget). A dedicated queue was tried mid-PR and removed: `async_insert` already batches server-side, so the queue only added durability, which isn't a requirement right now. The `EventSink` seam keeps a durable transport (e.g. a Redis-Streams buffer) easy to add later without touching producers. ## Out of scope S3 peer sink (seam only), Postgres or any second read path, `ReplicatedMergeTree`, ClickHouse table-schema changes, and the record-data `EVENT_STREAM_CHANNEL` (unchanged, separate concern). ## Testing Unit tests cover the registry definitions and row normalization, the entitlement gating, the envelope builders, and the producers. Integration tests cover the write paths (record create produces an `objectEvent`; the track mutation produces a `workspaceEvent`) and the read/query path across all five tables. Verified with typecheck, lint, a server boot, and GraphQL/SDK codegen. |
||
|
|
6c65d26ced |
feat(app-dev): add dry-run preview to dev sync (#21251)
Split out of #21240. Stacked on #21250 (review/merge that first). `yarn twenty dev --once --dry-run` computes the migration plan and prints the diff **without applying anything** (no migration, no app-record update, no SDK generation). Also renders the diff on a normal `dev --once` sync. <img width="646" height="179" alt="image" src="https://github.com/user-attachments/assets/59f3ddcd-2a5b-4b8a-b21a-c659abe16af0" /> |
||
|
|
bfb83e93b2 |
fix(metadata): resolve junction targets order-independently during mgration (#21193)
A junction relation points at a target field on the join object that another action may create later (two junctions into the same join reference each other). The builder validator now also looks up the target in the to be created set, and the runner mints every field id up front so the target resolves regardless of action order, the same way relation pairs are already handled. --------- Co-authored-by: Félix Malfait <felix@twenty.com> |
||
|
|
51e3eddb29 |
Add job queue latency histogram metric (#21260)
## Summary - Records the time each job spends waiting in the queue (enqueue → processing start) as an OpenTelemetry histogram - Metric is broken down by `queue` and `job_name` attributes, enabling per-queue p50/p95/p99 latency analysis - Uses BullMQ's native `job.timestamp` for accurate measurement ## Test plan - [x] Deploy to staging and verify `job/latency-ms` metric appears in ClickHouse `otel_metrics_histogram` table - [x] Confirm Grafana dashboard can query the histogram data |
||
|
|
27db8bbae4 |
fixes - remove billing cancellation at trial end + disable ai chat if suspended (#21261)
- Remove billing cancelation if trial is ended (subscription activated) - Disable AI Chat use if workspace suspended |
||
|
|
84ba7eb8dc |
fix(app-dev): serialize dev sync per workspace with a cache lock (#21250)
Split out of #21240. Stacked on #21249 (review/merge that first). Concurrent `syncApplication` calls on the same workspace could interleave their metadata migrations and leave metadata partially applied. Wrap the manifest sync in a per-workspace cache lock (`app-sync:<workspaceId>`), mirroring the install path. The rate-limit throttle stays outside the lock. --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
2dbdd72e65 |
chore: bump version to 2.11.0 (#21259)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
f20d04eb6e |
feat(app-dev): surface metadata diff in dev sync and name failing migration actions (#21249)
Split out of #21240. - Render the applied metadata changes (created/updated/deleted + identifiers) in the dev sync output instead of a bare `✓ Synced`. - Include the failing entity's `universalIdentifier` in `WorkspaceMigrationRunnerException` messages so conflicts are diagnosable. <img width="637" height="114" alt="image" src="https://github.com/user-attachments/assets/61422a16-370c-4e9b-a2f6-c29ce17f3b1b" /> <img width="497" height="104" alt="image" src="https://github.com/user-attachments/assets/d493c398-da29-49c9-ac5e-aa0f26cd7389" /> <img width="593" height="127" alt="image" src="https://github.com/user-attachments/assets/15e26edc-c0e4-4427-bd34-909040e970c9" /> --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
c3dd6b25a6 |
fix: use canonical oxlint rule id in lint-disable directives (#21253)
## What Many `oxlint-disable` / `eslint-disable` directives across the repo carry a corrupted rule id — `@typescripttypescript/<rule>` — most likely a find-and-replace accident that mangled the eslint-era `@typescript-eslint/` prefix. oxlint matches disable directives **loosely by rule name**, so these still suppress in practice (not a silent no-op), but the id is malformed and misleading. ## Change Replace them with the **canonical oxlint id** `typescript/<rule>` — matching the plugin name and rule keys declared in `.oxlintrc.json` — **127 files, 262 directives**: | rule | count | | --- | ----- | | `typescript/no-explicit-any` | 250 | | `typescript/ban-ts-comment` | 6 | | `typescript/no-misused-promises` | 4 | | `typescript/no-empty-object-type` | 2 | - `twenty-server`: 122 files - `twenty-front`: 5 files Comment-only — no code or runtime changes. ## Verification `oxlint --type-aware -c .oxlintrc.json` reports **0 warnings / 0 errors** for both `twenty-server` and `twenty-front`. Every changed line is exactly the id correction inside a disable directive (262 insertions / 262 deletions, no collateral edits). > Addresses the cubic review, which flagged that the canonical oxlint id is `typescript/...` (no `@`). Worth noting the original `@typescripttypescript/` was not actually a silent no-op — oxlint matches these directives loosely by rule name — but `typescript/` is the correct, config-aligned id. |
||
|
|
128d2d394d |
feat: allow apps to add view fields to existing views (defineViewField) (#21160)
## Summary
Lets a Twenty application add **view fields (columns) to an existing
view it does not own** — including standard views like the People index
view — without redeclaring/owning that view. This mirrors the existing,
working pattern by which an app adds a custom field to a standard object
via `defineField` + `objectUniversalIdentifier`.
The asymmetry being removed was purely in the manifest schema:
`ViewFieldManifest` only existed *nested* inside
`ViewManifest.fields[]`, so adding a view field forced declaring a
`ViewManifest` — which the sync treats as a view the app creates and
owns, and rejects when the UID is a standard view's. Validation,
persistence, the FK aggregator machinery, and uninstall cleanup were
already generic and cross-app-safe, so no engine changes were needed.
### Changes
- **twenty-shared:** new top-level `StandaloneViewFieldManifest`
(`ViewFieldManifest & { viewUniversalIdentifier }`),
`Manifest.viewFields`, and a `SyncableEntity.ViewField` member.
- **twenty-sdk:** `defineViewField` (validates `universalIdentifier` +
`viewUniversalIdentifier` + `fieldMetadataUniversalIdentifier`), CLI
manifest assembly of a top-level `viewFields` list, and `dev:add
viewField` scaffolding.
- **twenty-server:** one top-level loop over `manifest.viewFields` that
reuses the existing `fromViewFieldManifestToUniversalFlatViewField`
converter (already parameterized by `viewUniversalIdentifier`). No
validator/persistence/aggregator changes.
### Notes for maintainers
- Confirm the `Manifest.viewFields` optionality convention — implemented
as a **required** array to mirror `fields`/`views`.
- Two different apps adding a column for the same field to the same view
conflicts on the existing unique `(fieldMetadataId, viewId)` partial
index; the existing `flat-view-field-validator` duplicate check surfaces
this as a structured validation error.
- `dev:add viewField` scaffolding is included (was optional in the
plan).
## Test Plan
- [x] `twenty-shared` typecheck
- [x] `twenty-sdk` 364 unit tests + `buildManifest` assembly test
(rich-app fixture) + typecheck + prettier
- [x] `twenty-server` typecheck + `lint:diff-with-main`
- [x] **Server integration suite**
`successful-manifest-update-view-field.integration-spec.ts` (4/4):
- standalone view field attaches to the standard `allPeople` view
without recreating it (sync succeeds, no
`INVALID_VIEW_DATA`/`ENTITY_ALREADY_EXISTS`)
- uninstall removes the contributed column while the standard view + its
columns remain intact
- duplicate `(view, field)` rejected with `METADATA_VALIDATION_FAILED`
- unknown target view rejected
- [x] Sibling `successful-manifest-update-field.integration-spec.ts`
still green (no harness regression)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
979047d004 |
fix: allow email change verification on self-hosted instances (#20123)
fixes #20117 ## Technical Details Flow after fix: 1. User submits email change request 2. user.service.ts:517-524 calls sendVerificationEmail() with verificationTrigger: EMAIL_UPDATE 3. Guard checks: verificationTrigger === SIGN_UP → false → guard skipped 4. Verification token generated, email rendered and sent via emailService.send() 5. User receives confirmation email at new address 6. User clicks confirmation link → email update completes --- Impact - Minimal change: Only 3 lines modified in a single file - No breaking changes: Sign-up verification behavior unchanged - Security preserved: Email changes always require verification (correct security behavior) - Self-hosted friendly: Instance admins can disable sign-up verification while keeping email change verification active --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
8bd4cbc3fd |
fix(ai) - optim (#21233)
1. tool-registry.service.ts, Pass precomputed catalog to resolveSchemas() resolveSchemas() now accepts an optional precomputedCatalog parameter. Both getToolsByName() and getToolInfo() pass the catalog they already fetched, eliminating a redundant getCatalog() rebuild inside resolveSchemas(). 2. database-tool.provider.ts, Skip field lookup when schemas=false When building the catalog index (includeSchemas=false), getFlatFieldsFromFlatObjectMetadata() is no longer called for each of the 25 objects. The hasGroupByToolInputSchema() check is also skipped, group_by tools are always included in the index, with the real eligibility check deferred to learn_tools time. --> 100/150ms gain on learn/execute_tool execution |
||
|
|
0877ba2ffd |
Fix getApplicationSubAllFlatEntityMaps to prune unrelated appIds universal identifier aggregators (#21234)
# Introduction Atm when computing the `fromAllFlatEntityMaps` we're retrieving all the applicationIds related metadata entities to build a flat entity maps scoped to them ( atm always the applicationId + twenty-standard application id ) only inter app dependency we manage for the moment A flat entity contains universal identifier aggregator to its related entities The issue was that the `getApplicationSubAllFlatEntityMaps` wasn't pruning the aggregator by app Now added a new process phase after the initial one that will check that all the aggregators contains universal identifiers that has been retrieve from the appId + appId standard intersection ## TDD test Created a very human readable ( that's a joke ) test |