Files
martmull 8684e7b6ed Type composite raw JSON sub-fields in the workspace GraphQL schema (#25116)
Follow-up to https://github.com/twentyhq/twenty/pull/24869, closes
twentyhq/core-team-issues#2748.

## Why

#24869 fixed `additionalEmails` typing by post-processing the generated
client SDK: a static map of composite type/field names rewrote
`Scalars['JSON']` into a hand-written TypeScript shape after codegen.
That kept the wrong type in the schema itself and only patched one
consumer.

The root cause is the schema: composite sub-fields stored as `RAW_JSON`
(`Emails.additionalEmails`, `Phones.additionalPhones`,
`Links.secondaryLinks`, `Actor.context`) are exposed as the opaque
`JSON` scalar even though their shape is known.

## What

- Reverts #24869 (`composite-field-type-overrides.ts`, its tests and the
genql engine snapshot test).
- The workspace schema builder now maps those four sub-fields to real
GraphQL types instead of the JSON scalar:
  - `additionalEmails: [String!]`
- `additionalPhones: [AdditionalPhone!]` / `[AdditionalPhoneInput!]`
(`number`, `callingCode`, `countryCode`)
- `secondaryLinks: [SecondaryLink!]` / `[SecondaryLinkInput!]` (`label`,
`url`)
  - `context: ActorContext` / `ActorContextInput` (`provider`)
Same pattern as the existing `FILES` field type, which already resolves
a jsonb column through `FileObject`. Nested fields are nullable so
partially filled rows stay readable. Filter, order-by and group-by
inputs are untouched: they never expose the raw value.
- Direct execution returned the stored value as-is for composite
sub-fields, so a nested selection got back the raw jsonb without
`__typename`. `graphQLFormatResultFromSelectedFields` now projects the
selected sub-fields of an object-typed composite sub-field and fills in
its `__typename`; a value that is not an object (a legacy JSON string,
say) still passes through untouched.
- Front: composite selections in `mapFieldMetadataToGraphQLQuery`
request the nested sub-fields, and `sanitizeRecordInput` strips
`__typename` from composite values before sending them as mutation input
(Apollo adds it to every nested selection; composite input types do not
accept it). Optimistic cache writes for `LINKS` carry the nested
`__typename`.
- Server/e2e test gql field constants updated to the nested selections.

Generated client types, straight from the schema now:

```ts
additionalEmails?: string[]
additionalPhones?: AdditionalPhone[]
secondaryLinks?: SecondaryLink[]
context?: ActorContext
```

## Compatibility

The workspace API runs through direct execution, which does not validate
documents or variables against the generated schema, so existing queries
selecting `additionalPhones` / `secondaryLinks` / `createdBy.context` as
leaves keep working and keep returning the stored value, and mutations
still accept what they accepted before. What changes is the published
schema: introspection, SDL and anything generated or validated from them
(the client SDK above, schema-aware tooling) now see object types where
they saw `JSON`.

`api-breaking-changes` diffs the introspected schema and counts a field
type change as breaking, so it fails on this PR by design. Keeping it
green would mean leaving the JSON fields in place and adding parallel
typed ones, which is the opposite of the intent here — flagging it for
reviewers rather than working around it.

## Test

- Unit test asserting the composite output and create-input types render
as the real types.
- Unit test for the direct-execution formatter: nested sub-field
projection with `__typename`, and passthrough for a non-object value.
- `twenty-front` and `twenty-server` unit suites pass (remaining
failures in this environment are unbuilt sibling packages:
`twenty-emails`, `twenty-client-sdk/generate`,
`twenty-front-component-renderer`).
- Generated a client from a schema with the new composite types and
checked the emitted TypeScript matches the shapes above.
- Integration and e2e suites were not run locally (no database); the
phone metadata suite's expectations were corrected from the CI run.
2026-09-01 13:20:49 +00:00
..