## Summary
- `upgrade:1-21:backfill-message-thread-subject` was failing on every
workspace with `Method not allowed because permissions are not
implemented at datasource level`.
- The global workspace datasource gates raw `query()` calls behind
`shouldBypassPermissionChecks`. Both the column-existence probe and the
UPDATE in this command now pass that flag, matching the pattern used by
the other 1.20/1.21 upgrade commands.
## Test plan
- [ ] Re-run `yarn command:prod
upgrade:1-21:backfill-message-thread-subject` and confirm all workspaces
complete without the permissions error
- [ ] Spot-check a workspace to confirm `messageThread.subject` is
backfilled from the most recent message
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Fixes#19070
The `neq` operator in `compute-where-condition-parts.ts` uses `OR` where
it should use `AND` when handling null-equivalent values.
Currently generates:
```sql
field != '' OR field IS NOT NULL
```
For a row where `field = ''`:
- `'' != ''` = false
- `'' IS NOT NULL` = true
- `false OR true` = true -- row incorrectly passes the filter
The `eq` operator correctly uses `OR field IS NULL` because it's
additive (match value or its null equivalent). By De Morgan's law, the
negation `neq` needs `AND field IS NOT NULL` -- exclude if the value
doesn't match AND is not a null equivalent.
With the fix:
```sql
field != '' AND field IS NOT NULL
```
- `'' != ''` = false, `'' IS NOT NULL` = true, `false AND true` = false
-- correctly excluded
- `NULL != ''` = NULL, `NULL IS NOT NULL` = false, `NULL AND false` =
false -- correctly excluded
- `'Alice' != ''` = true, `'Alice' IS NOT NULL` = true, `true AND true`
= true -- correctly included
Affects `neq` filters on TEXT fields and all composite sub-fields
(firstName, lastName, primaryEmail, primaryPhoneNumber, address
sub-fields, etc.) when filtering against null-equivalent values.
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
## Summary
- **Inline email reply**: Replace external email client redirects
(Gmail/Outlook deeplinks) with an in-app email composer. Users can reply
to email threads directly from the email thread widget or via the
command menu.
- **SendEmail GraphQL mutation**: New backend mutation that reuses
`EmailComposerService` for body sanitization, recipient validation, and
SMTP dispatch via the existing outbound messaging infrastructure.
- **Side panel compose page**: Command menu "Reply" action now opens a
side-panel compose email page with pre-filled To, Subject, and
In-Reply-To fields.
### Backend
- `SendEmailResolver` with `SendEmailInput` / `SendEmailOutputDTO`
- `SendEmailModule` wired into `CoreEngineModule`
- Reuses `EmailComposerService` + `MessagingMessageOutboundService`
### Frontend
- `EmailComposer` / `EmailComposerFields` components
- `useSendEmail`, `useReplyContext`, `useEmailComposerState` hooks
- `useOpenComposeEmailInSidePanel` + `SidePanelComposeEmailPage`
- `EmailThreadWidget` inline Reply bar with toggle composer
- `ReplyToEmailThreadCommand` now opens side-panel instead of external
links
### Seeds
- Added `handle` field to message participant seeds for realistic email
addresses
- Seed `connectedAccount` and `messageChannel` in correct batch order
## Test plan
- [ ] Open an email thread on a person/company record → verify
"Reply..." bar appears below the last message
- [ ] Click "Reply..." → composer opens inline with pre-filled To and
Subject
- [ ] Type a message and click Send → email is sent via SMTP, composer
closes
- [ ] Use command menu Reply action → side panel opens with compose
email page
- [ ] Verify Send/Cancel buttons work correctly in side panel
- [ ] Test with Cc/Bcc toggle in composer fields
- [ ] Verify error handling: invalid recipients, missing connected
account
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
- Introduces a pluggable `WebSearchDriver` abstraction (interface,
factory, service, module) so web search is no longer tied to native
provider tools (Anthropic/OpenAI)
- **Exa** is the first driver implementation with support for
category-filtered search (company, people, news, research paper, etc.) —
particularly useful for CRM workflows
- Per-query billing for both Exa ($0.007/query) and native provider
surcharges ($0.01/query for Anthropic/OpenAI) via the existing
`USAGE_RECORDED` pipeline
- New config variables: `WEB_SEARCH_DRIVER` (EXA/DISABLED),
`EXA_API_KEY`, `WEB_SEARCH_PREFER_NATIVE` (default false — prefers Exa
over native when both available)
- `WEB_SEARCH` operation type added for usage tracking and Stripe
metering
### Architecture
```
WebSearchDriver (interface)
├── ExaDriver — Exa neural search with category support
└── DisabledDriver — throws when search is disabled
WebSearchDriverFactory (extends DriverFactoryBase)
└── creates driver based on WEB_SEARCH_DRIVER config
WebSearchService (facade)
├── search(query, options?, billingContext?)
├── isEnabled()
└── emits USAGE_RECORDED events per query
WebSearchTool (Tool implementation)
└── registered in ActionToolProvider, available via tool catalog
```
### Native search billing gap fixed
Anthropic and OpenAI both charge $0.01/search on top of token costs. The
token costs were already billed, but the per-call surcharge was not.
Added `countNativeWebSearchCallsFromSteps` utility +
`billNativeWebSearchUsage` to `AiBillingService`, wired into both chat
and workflow agent paths.
## Test plan
- [ ] Set `WEB_SEARCH_DRIVER=EXA` + `EXA_API_KEY=...` and verify AI chat
can search the web
- [ ] Verify category parameter works (ask about a specific
company/person)
- [ ] Set `WEB_SEARCH_DRIVER=DISABLED` and verify search tool is not
exposed
- [ ] Set `WEB_SEARCH_PREFER_NATIVE=true` with Anthropic model and
verify native search is used
- [ ] Verify usage events are emitted in ClickHouse for both Exa and
native search paths
- [ ] Verify existing billing tests pass (`npx jest
ai-billing.service.spec.ts`)
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
## Summary
- Move email thread display from side panel to a dedicated record page
with a new `EMAIL_THREAD` widget type
- Add message thread as a standard object with page layout, subject
field, and backfill command
- Add reply-to-email command menu item for message thread records
- Remove old side panel message thread components in favor of the new
widget-based approach
## Type fixes
- Add `EMAIL_THREAD` to `WidgetConfigurationType`, `WidgetType`, and all
configuration/validator maps
- Create `EmailThreadConfigurationDTO` and shared
`EmailThreadConfiguration` type
- Register EMAIL_THREAD in widget type validators, configuration
resolvers, and standard widget mappings
## Test plan
- [ ] Verify message thread record pages render with the email thread
widget
- [ ] Verify email thread preview navigates to the record page instead
of opening side panel
- [ ] Verify reply-to-email command appears for message thread records
- [ ] Verify typecheck passes for both twenty-front and twenty-server
- [ ] Run existing test suites to check for regressions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This PR migrates `messageFolder`.`parentFolderId` from an internal
`UUID` reference to external provider id.
Eliminates unnecessary lookup and complexity
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
# Introduction
Typeorm migration are now associated to a given twenty-version from the
`UPGRADE_COMMAND_SUPPORTED_VERSIONS` that the current twenty core engine
handles
This way when we upgrade we retrieve the migrations that need to be run,
this will be useful for the cross-version incremental upgrade so we
preserve sequentiality
## What's new
To generate
```sh
npx nx database:migrate:generate twenty-server -- --name add-index-to-users
```
To apply all
```sh
npx nx database:migrate twenty-server
```
## Next
Introduce slow and fast typeorm migration in order to get rid of the
save point pattern in our code base
Create a clean and dedicated `InstanceUpgradeService` abstraction
The minimalMetadata query was returning untranslated English labels for
standard objects (Companies, People, Opportunities, etc.) when users
requested zh-CN locale. This fix adds translation logic to
MinimalMetadataService using the existing I18nService.
Changes:
- Add translateLabel() method to translate standard object labels
- Pass locale from GraphQL context through resolver to service
- Only translate non-custom objects (custom objects use user-defined
labels)
Co-authored-by: Kevin Yu <kevin.yu@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).
This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.
**Please review before merging** — verify no critical models were
incorrectly deprecated.
Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
## Summary
- **Remove `ExecuteToolResult` wrapper** — `execute_tool` is now a
transparent dispatcher that returns the raw `ToolOutput` from underlying
tools. No more `{ toolName, result }` envelope.
- **Type the entire execution chain as `Promise<ToolOutput>`** — from
`ToolExecutorService.dispatch()` through `resolveAndExecute()` to
`execute_tool.execute()`. Zero `Promise<unknown>` remaining in the tool
layer.
- **Use `Extract<ToolExecutionRef, ...>`** for dispatch methods,
enabling exhaustive switch checking and removing `as never` casts.
- **Relax `ToolOutput.result` to accept `null`** — removes `??
undefined` hacks at the boundary with logic function results.
- **Enforce 1-export-per-file** across tool type/interface files (split
`tool-descriptor.type.ts`, `tool-provider.interface.ts`, `tool.type.ts`,
`tool-output.type.ts`, `tool-executor.service.ts`).
- **Simplify error handling** — `wrapWithErrorHandler` and all
meta-errors (tool not found, tool excluded) now return consistent
`ToolOutput` shape with `error` as a plain string.
- **Frontend reads output directly** — removed `unwrapToolOutput`
utility; `ToolStepRenderer` and `ThinkingStepsDisplay` extract
`message`/`error` from the raw output with simple type guards.
- **Add permission error detection** for email tools via
`isInsufficientPermissionsError`, guiding the AI model to suggest
account reconnection instead of hallucinating about visibility settings.
## Test plan
- [ ] AI chat tool calls return visible output (not "null") in the UI
- [ ] Tool errors display correctly in the JSON tree
- [ ] Email draft/send tools return actionable permission errors
- [ ] Code interpreter output renders correctly
- [ ] Thinking steps display tool outputs properly
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Bug fixes exposed by always-on direct execution
1. GraphQL spec compliance — data[field] = null on resolver error
direct-execution.service.ts — Changed from Promise.allSettled (which
lost the responseKey on rejection) to Promise.all with per-field
try/catch; errors now set data[responseKey] = null per spec
2. Empty object arguments skipped (extractArgumentsFromAst)
extract-arguments-from-ast.util.ts — Removed isEmptyObject check;
filter: {}, data: {} now correctly passed to resolvers instead of
silently dropped (which caused permissions to never be checked)
3. orderBy: {} factory default treated as "no ordering"
direct-execution.service.ts — Before calling the resolver, strips
orderBy: {} and orderByForRecords: {} (empty-object factory defaults
that mean "no ordering")
assert-find-many-args.util.ts / assert-group-by-args.util.ts — Accept {}
for orderBy without throwing
4. orderBy: { field: '...' } object auto-coerced to [{ field: '...' }]
array
direct-execution.service.ts — Applies GraphQL list coercion: a
non-array, non-empty orderBy object is wrapped in an array before
assertion and resolver call
5. totalCount and aggregate fields returned as strings from PostgreSQL
graphql-format-result-from-selected-fields.util.ts — Added
coerceAggregateValue that parses numeric strings to numbers for
totalCount, sum*, avg*, min*, max*, count*, percentageOf* fields
Test updates
nested-relation-queries.integration-spec.ts — Updated expected error
message from Yoga schema-validation message to direct execution resolver
message
~30 snapshot files — Updated to reflect direct execution's error
messages (different from Yoga schema-validation messages for input type
errors)
- WorkflowCronTriggerCronJob was querying every active workspace (~700)
sequentially every minute to find cron triggers, causing regular CPU
spikes to 100% on worker pods
- Added a Redis hashset cache that stores cron triggers. On cache miss
(TTL expired, cold start, or explicit invalidation), a full scan
rebuilds the cache
- Creating/deleting a new cron trigger updates the cache, only if exists
Fixes https://github.com/twentyhq/twenty/issues/19309
In exclusion mode (select all), selectedRecords is always [] since
individual record IDs aren't tracked. The noneDefined()/everyDefined()
checks return false on empty arrays by design, which hides the delete,
restore, and destroy commands from the command menu.
- Wrap selectedRecords array checks with (isSelectAll or ...) to bypass
when in exclusion mode
- Remove the `numberOfSelectedRecords < 10000` limit
- Add `upgrade:1-21:fix-select-all-command-menu-items` command to
backfill existing workspaces
- Add tests
- simplify the base application template
- remove --exhaustive option and replace by a --example option like in
next.js https://nextjs.org/docs/app/api-reference/cli
- Fix some bugs and logs
- add a post-card app in twenty-apps/examples/
## Summary
Rebased version of #19051 with all review comments addressed. Clean
branch on latest main, lint/typecheck/tests passing.
### Changes from original PR
- AI can now create, update, and delete **view filters**
(`ViewFilterToolsFactory`) and **view sorts** (`ViewSortToolsFactory`)
- `create_view` now accepts `calendarFieldName`, `calendarLayout`, and
`fieldNames` to configure views at creation time
- Three new standard skills: `view-building`, `view-filters-and-sorts`,
`custom-objects-cleanup`
- `workspace-demo-seeding` skill reworked to keep standard objects and
enrich them with custom fields
- Cache invalidation for nav menu items when object `isActive` changes
- Dashboard tool descriptions improved (RECORD_TABLE widget workflow)
### Review comments addressed (all 10 from #19051)
1. **Sentry + Cubic**: Calendar field DATE/DATE_TIME validation — added
`resolveCalendarFieldMetadataId` using `isFieldMetadataDateKind`
2. **Cubic**: "navigate tool" → "navigate_app tool" in skill metadata
(all 7 occurrences)
3. **Copilot**: KANBAN views now require `mainGroupByFieldName` — throws
clear error if missing
4. **Copilot**: CALENDAR views now require both `calendarFieldName` and
`calendarLayout` — validated before DB call
5. **Copilot**: Mock field fixtures include `type` property (DATE_TIME,
TEXT, SELECT)
6. **Copilot**: `ViewFilterValue` type assertion instead of unsafe `as
string` casts (3 locations)
7. **FelixMalfait**: Removed
`NavigationMenuItemObjectDeactivationListener` — replaced with cache
invalidation
8. **FelixMalfait**: Consolidated `ViewFilterToolProvider` and
`ViewSortToolProvider` into single `ViewToolProvider`
9. Removed `VIEW_FILTER` and `VIEW_SORT` from `ToolCategory` enum
(merged into `VIEW`)
10. Removed stale `existingFeatureFlagsMap` param incompatible with
current main
## Test plan
- [x] `npx nx lint:diff-with-main twenty-server` — passes
- [x] `npx nx typecheck twenty-server` — passes
- [x] `view-tools.factory.spec.ts` — all 20 tests pass (including 3 new
validation tests)
Supersedes #19051https://claude.ai/code/session_01QPV74NU6vzmJb32e4i899E
---------
Co-authored-by: Claude <noreply@anthropic.com>
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).
This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.
**Please review before merging** — verify no critical models were
incorrectly deprecated.
Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
## Summary
- **Make app-synced objects searchable**: `isSearchable` was hardcoded
to `false` and the `searchVector` field was missing the `GENERATED
ALWAYS AS (...)` expression, causing all records to have a `NULL` search
vector and be excluded from search results. Fixed by defaulting
`isSearchable` to `true` (configurable via the object manifest),
computing the `asExpression` from the label identifier field, and
allowing the update-field-action-handler to handle the `null` → defined
`asExpression` transition.
- **Make `isSearchable` updatable on an object**: The property had
`toCompare: false` in the entity properties configuration, so updates
via the API were silently ignored and never persisted. Fixed by setting
`toCompare: true`.
Auto merged passed to fast on
https://github.com/twentyhq/twenty/pull/19271
Try catch has been added while debugging an integration test suite
covering the workspace creation that would fail due to sdk generation
failure
As they're run synchronously in integration tests they would stop the
workspace creation process
If the proposed fix here isn't enough I'll wrapp the run sync test to be
catching any unexpected issue
## Summary
- Adds `IS_AI_ENABLED` to `PUBLIC_FEATURE_FLAGS` so it appears in
**Settings > Lab** as a self-serve toggle for users
- Includes a cover image (`is-ai-enabled.png`) hosted on
`twenty.com/images/lab/`, following the existing convention for lab
feature flag images
- AI is now ready for public beta — users can enable it directly from
the Lab
## Test plan
- [ ] Verify the AI card appears in Settings > Lab with the cover image
banner
- [ ] Toggle AI on/off and confirm the feature flag is persisted
- [ ] Confirm AI features (agent chat, etc.) activate when the flag is
enabled
Made with [Cursor](https://cursor.com)
Avoid overloading the workspace creation with the standard and custom
app sdk generation, do through a job
```ts
[Nest] 90397 - 04/02/2026, 4:44:20 PM LOG [CoreEntityCacheService] Cache stats: localCacheSize=0 memoizerCacheSize=0 memoizerPendingSize=0 entriesByKey={} versionsByKey={}
[Nest] 90397 - 04/02/2026, 4:45:03 PM LOG [BullMQDriver] Processing job 4325 with name CallWebhookJobsForMetadataJob on queue webhook-queue
[Nest] 90397 - 04/02/2026, 4:45:03 PM LOG [BullMQDriver] Processing job 1 with name GenerateSdkClientJob on queue workspace-queue
[Nest] 90397 - 04/02/2026, 4:45:03 PM LOG [BullMQDriver] Job 1 with name GenerateSdkClientJob processed on queue workspace-queue in 0.64ms
[Nest] 90397 - 04/02/2026, 4:45:03 PM LOG [BullMQDriver] Processing job 2 with name GenerateSdkClientJob on queue workspace-queue
[Nest] 90397 - 04/02/2026, 4:45:03 PM LOG [BullMQDriver] Job 2 with name GenerateSdkClientJob processed on queue workspace-queue in 0.06ms
[Nest] 90397 - 04/02/2026, 4:45:03 PM LOG [BullMQDriver] Job 4325 with name CallWebhookJobsForMetadataJob processed on queue webhook-queue in 17.47ms
[Nest] 90397 - 04/02/2026, 4:45:03 PM LOG [BullMQDriver] Processing job 4326 with name CallWebhookJobsForMetadataJob on queue webhook-queue
```
- Replace soft-deletion (deletedAt) with isActive boolean for
overridable entities (tabs, widgets, viewFieldGroups, viewFields).
Standard entities are deactivated (isActive: false) when removed from
update payloads, while custom entities are hard-deleted.
- When a viewFieldGroup is deactivated/deleted, its viewFields are
reassigned to the next section by position (or null if none remain).
- Add isActive: true filters to viewFieldGroup and viewField API queries
so deactivated entities are excluded from responses.
Next:
- fields-widget-upsert.service.ts should be refactored a bit
- Add restore logic
Insert operations blocked by field-level update permissions on
non-editable fields
The insert code path in permissions.utils.ts fell through to the update
case (no break), causing validateUpdateFieldPermissionOrThrow to reject
inserts when any field had "Edit disabled" which could conflict with RLS
predicates (used for insertion of new records)
Fixes https://github.com/twentyhq/twenty/issues/19201
We will keep checking update permissions for insertion (until we decide
to have a separate permission flag for insertion) but to fix the issue
we will skip this part if it conflicts with an RLS predicate
# Introduction
Currently preparing the `UpgradeCommand` refactor, in this way started
by cleaning up the existing avoiding unecessary dependencies to others
services allowing easier readability and concern centralization for
upcoming refactor
The UpgradeCommand was extending up to five classes, overriding
abstracted class and so on. It was also cascade
injecting 3 services
Introducing the `WorkspaceIteratorService` that centralize the commands
set to run over a single workspace logic shared between both atomic
upgrade command call and global upgradeCommand
## Tradeoff
Duplicated `@Option` between both `UpgradeCommandRunner` and
`WorkspaceMigrationRunner`
## Before
```
UpgradeCommand
└─ extends UpgradeCommandRunner
└─ extends ActiveOrSuspendedWorkspacesMigrationCommandRunner
└─ extends WorkspacesMigrationCommandRunner (owns workspace iteration loop + ORM deps)
└─ extends MigrationCommandRunner (dry-run, verbose, error handling)
└─ extends CommandRunner (nest-commander)
```
## Now
```
UpgradeCommand
└─ extends UpgradeCommandRunner
└─ extends CommandRunner (nest-commander)
uses ─► Services (via composition)
```
## Logging management
At the moment all services are logging, in the best of the world only
the runners should be doing so
The backend validation for field permissions was using !== null to check
canReadFieldValue and canUpdateFieldValue, but these optional GraphQL
fields can also be undefined when omitted from the input. This caused
the validation to incorrectly reject legitimate requests (e.g.,
restricting only "update" without specifying "read") with the error
"Field permissions can only be used to restrict access, not to grant
additional permissions."
Replaced !== null checks with isDefined() so that both null and
undefined are treated as "no opinion" on that permission.
To reproduce:
- Create a single FieldPermission on an object with canEdit: false
without any other rule on that same object
## Summary
- Set `accent="blue"` on InformationBanner action button so it renders
blue instead of default gray
- Add Banner storybook stories for all color × variant combinations
(BluePrimary, BlueSecondary, DangerPrimary, DangerSecondary)
- Use `useUserTimezone()` in `SettingsDatePickerInput` instead of
browser timezone (`Temporal.Now.timeZoneId()`) so dates respect the
admin's profile timezone preference
- Separate `onChange` from `onClose` in `SettingsDatePickerInput` so
changing the hour no longer forces the date picker to close
## Summary
- **Apollo factory** (`apollo.factory.ts`): Bail early with `EMPTY` when
no token pair exists so no request is forwarded without credentials.
Propagate renewal success/failure as a boolean so failed renewals stop
the operation chain instead of forwarding with stale tokens.
- **Refresh token service** (`refresh-token.service.ts`): When a revoked
refresh token is reused past the grace period, reject only that token
instead of mass-revoking all user tokens. The most common cause is a
lost renewal response (e.g. navigation during refresh), not actual token
theft. This eliminates the "Suspicious activity detected" errors users
were seeing. Also switches to `findOneBy` since the `appTokens` relation
is no longer needed.
## Test plan
- [x] `refresh-token.service.spec.ts` — all 7 tests pass
- [ ] Verify login flow: sign in from `app.localhost`, get redirected to
workspace subdomain without "Suspicious activity" errors
- [ ] Verify token renewal: let access token expire, confirm silent
renewal works and operations resume
- [ ] Verify concurrent tabs: open multiple tabs, let tokens expire,
confirm no mass revocation cascade
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
## Summary
- Adds an index on `viewField.viewFieldGroupId` to fix a ~28s `DELETE
FROM core.viewFieldGroup WHERE workspaceId = $1` query observed in
production
- The slow query is triggered during workspace hard deletion
(`deleteWorkspaceSyncableMetadataEntities` in `workspace.service.ts`)
- Root cause: the FK constraint `viewField.viewFieldGroupId →
viewFieldGroup.id` with `ON DELETE SET NULL` forces PostgreSQL to
sequentially scan the entire `viewField` table for every deleted
`viewFieldGroup` row, because no index exists on the referencing column
- Uses `CREATE INDEX CONCURRENTLY` in the migration to avoid locking the
table on production
### Context
GraphQL introspection queries (__schema, __type) were going through the
full Yoga server pipeline, which forces a complete workspace schema
build, loading flat metadata maps, building all GraphQL types, wiring
all resolver factories, and calling makeExecutableSchema. This is
expensive, even though introspection only needs the type structure and
zero resolver execution.
A new WorkspaceGraphqlSchemaSDLService extracts the SDL computation that
was previously embedded inside WorkspaceSchemaFactory.
From `direct-execution.service.ts` :
- `buildSchema(sdl)` reconstructs a resolver-free GraphQLSchema from the
SDL
- pure CPU, not cached, not sure it worths it ?
- `execute({ schema, document, variableValues })` from graphql-js,
introspection is answered entirely by the graphql-js runtime from type
metadata, no resolver execution needed
#### Nice to do ?
- Use new cache service for typeDefs ?
### Renaming bonus: `typeDefs` → `sdl`
`typeDefs` is an Apollo/graphql-tools convention. It's the parameter
name in
`makeExecutableSchema({ typeDefs, resolvers })`, not a native GraphQL
spec term.
In proper GraphQL semantics, what this service produces is the **SDL**
(Schema
Definition Language): the official term for the string representation of
a schema.
`printSchema()` produces it, `buildSchema()` consumes it.
##### Why the distinction matters
- **Type definitions** implies partial type declarations (objects,
scalars, enums…)
- **Schema SDL** conveys a *complete* schema document: all types
**plus** the root
operation types (`Query`, `Mutation`) which is exactly what
`printSchema(schema)`
produces