From 37b986aa4b9b5606267eccffa05467f04c2a9a75 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 8 Jun 2026 20:46:45 +0200 Subject: [PATCH] security: vendor @genql/cli codegen to drop undici/native-fetch (#21339) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Vendors a narrowed copy of [`@genql/cli@3.0.5`](https://github.com/remorses/genql) (MIT) into `packages/twenty-client-sdk/src/generate/genql/` and repoints the two client generators at it, then removes `@genql/cli` from `twenty-client-sdk`, `twenty-sdk` and `create-twenty-app`. ## Why `@genql/cli` was used **only** to generate the typed GraphQL client from an SDL string. It is unmaintained and pulls in vulnerable/abandoned transitives — `undici@5` (**30 Dependabot alerts**), `native-fetch`, `listr`, `yargs`, etc. None of these were ever executed by Twenty: the sole consumer of `undici`/`native-fetch` is `@genql/cli`'s live-endpoint schema-introspection path, and Twenty always passes a schema string, never an endpoint. Removing the package eliminates the dependency at the source — for Twenty and for scaffolded end-user apps. ## What changed vs upstream The vendored copy (`genql/README.md` + `genql/LICENSE`) keeps the `render/` and `runtime/` trees verbatim and narrows the orchestration: - **Dropped the endpoint/introspection path** (`schema/fetchSchema.ts`) — the only `undici`/`native-fetch`/`qs` consumer. - **Dropped `listr`** — generation tasks run as plain sequential `async` functions (file contents unchanged). - **Replaced `fs-extra`/`mkdirp`/`rimraf`** with `node:fs`. - **Runtime templates are imported as `?raw`** and bundled, instead of read from `node_modules` at generation time. - **Kept `prettier@^2.8` and `@graphql-tools/*`** so the generated output is byte-for-byte identical. ## Verification - **Byte-identical output**: regenerating the metadata client from its committed schema produces a recursive-diff-clean result vs the previous `@genql/cli` output (including the copied `runtime/` folder). The core client generates and esbuild-bundles cleanly. - The public `twenty-client-sdk/generate` barrel API is unchanged (twenty-server / twenty-sdk consumers unaffected). - `undici@^5`, `native-fetch`, `@genql/cli`, `listr`, `yargs@^15` and `subscriptions-transport-ws@0.9` are gone from `yarn.lock` (net −364 lines). - `twenty-client-sdk` and `twenty-sdk` typecheck, lint and build; `twenty-client-sdk` tests pass (9/9). ## Notes - The vendored folder is excluded from `oxlint`/`oxfmt` (it is third-party code, with `@ts-nocheck` on the verbatim renderers, mirroring the generated output). - Stacks conceptually on #21334 (drops `@genql/runtime`); the two are independent and only overlap trivially in `yarn.lock`. `@genql/runtime` is intentionally left for that PR. --- packages/create-twenty-app/package.json | 1 - packages/twenty-client-sdk/.oxlintrc.json | 2 +- packages/twenty-client-sdk/.prettierignore | 1 + packages/twenty-client-sdk/package.json | 7 +- .../__tests__/generated-client-query.test.ts | 156 ++++++ .../src/generate/generate-core-client.ts | 2 +- .../src/generate/generate-metadata-client.ts | 2 +- .../src/generate/genql/LICENSE | 21 + .../src/generate/genql/README.md | 45 ++ .../src/generate/genql/config.ts | 19 + .../src/generate/genql/helpers/files.ts | 36 ++ .../src/generate/genql/helpers/prettify.ts | 17 + .../src/generate/genql/index.ts | 2 + .../src/generate/genql/main.ts | 14 + .../genql/render/client/renderClient.ts | 158 ++++++ .../genql/render/common/RenderContext.ts | 107 ++++ .../generate/genql/render/common/comment.ts | 37 ++ .../genql/render/common/excludedTypes.ts | 11 + .../genql/render/common/relativeImportPath.ts | 7 + .../genql/render/common/renderTyping.ts | 63 +++ .../generate/genql/render/common/support.ts | 16 + .../render/requestTypes/inputObjectType.ts | 22 + .../genql/render/requestTypes/objectType.ts | 111 +++++ .../render/requestTypes/renderRequestTypes.ts | 65 +++ .../render/requestTypes/requestTypeName.ts | 7 + .../genql/render/requestTypes/unionType.ts | 29 ++ .../genql/render/responseTypes/enumType.ts | 9 + .../render/responseTypes/interfaceType.ts | 56 +++ .../genql/render/responseTypes/objectType.ts | 69 +++ .../responseTypes/renderResponseTypes.ts | 71 +++ .../genql/render/responseTypes/scalarType.ts | 15 + .../render/responseTypes/typeMappedAlias.ts | 33 ++ .../genql/render/responseTypes/unionType.ts | 43 ++ .../genql/render/schema/renderSchema.ts | 7 + .../render/typeGuards/renderTypeGuards.ts | 40 ++ .../genql/render/typeMap/objectType.ts | 67 +++ .../genql/render/typeMap/renderTypeMap.ts | 140 ++++++ .../genql/render/typeMap/scalarType.ts | 9 + .../generate/genql/render/typeMap/support.ts | 7 + .../genql/render/typeMap/unionType.ts | 22 + .../src/generate/genql/runtime-templates.ts | 25 + .../src/generate/genql/runtime/batcher.ts | 270 ++++++++++ .../generate/genql/runtime/createClient.ts | 67 +++ .../src/generate/genql/runtime/error.ts | 28 ++ .../src/generate/genql/runtime/fetcher.ts | 97 ++++ .../genql/runtime/generateGraphqlOperation.ts | 224 +++++++++ .../src/generate/genql/runtime/index.ts | 12 + .../src/generate/genql/runtime/linkTypeMap.ts | 138 ++++++ .../generate/genql/runtime/typeSelection.ts | 97 ++++ .../src/generate/genql/runtime/types.ts | 68 +++ .../src/generate/genql/tasks/clientTasks.ts | 103 ++++ .../src/generate/genql/tasks/schemaTask.ts | 36 ++ .../src/metadata/generated/runtime/batcher.ts | 6 + packages/twenty-client-sdk/vite.config.ts | 1 + packages/twenty-sdk/package.json | 1 - yarn.lock | 465 +++++------------- 56 files changed, 2835 insertions(+), 349 deletions(-) create mode 100644 packages/twenty-client-sdk/src/generate/__tests__/generated-client-query.test.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/LICENSE create mode 100644 packages/twenty-client-sdk/src/generate/genql/README.md create mode 100644 packages/twenty-client-sdk/src/generate/genql/config.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/helpers/files.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/helpers/prettify.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/index.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/main.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/client/renderClient.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/common/RenderContext.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/common/comment.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/common/excludedTypes.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/common/relativeImportPath.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/common/renderTyping.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/common/support.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/requestTypes/inputObjectType.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/requestTypes/objectType.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/requestTypes/renderRequestTypes.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/requestTypes/requestTypeName.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/requestTypes/unionType.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/responseTypes/enumType.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/responseTypes/interfaceType.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/responseTypes/objectType.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/responseTypes/renderResponseTypes.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/responseTypes/scalarType.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/responseTypes/typeMappedAlias.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/responseTypes/unionType.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/schema/renderSchema.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/typeGuards/renderTypeGuards.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/typeMap/objectType.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/typeMap/renderTypeMap.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/typeMap/scalarType.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/typeMap/support.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/render/typeMap/unionType.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/runtime-templates.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/runtime/batcher.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/runtime/createClient.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/runtime/error.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/runtime/fetcher.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/runtime/generateGraphqlOperation.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/runtime/index.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/runtime/linkTypeMap.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/runtime/typeSelection.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/runtime/types.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/tasks/clientTasks.ts create mode 100644 packages/twenty-client-sdk/src/generate/genql/tasks/schemaTask.ts diff --git a/packages/create-twenty-app/package.json b/packages/create-twenty-app/package.json index 1196436f99..2c67f34fe4 100644 --- a/packages/create-twenty-app/package.json +++ b/packages/create-twenty-app/package.json @@ -28,7 +28,6 @@ }, "license": "AGPL-3.0", "dependencies": { - "@genql/cli": "^3.0.3", "chalk": "^5.3.0", "commander": "^12.0.0", "fs-extra": "^11.2.0", diff --git a/packages/twenty-client-sdk/.oxlintrc.json b/packages/twenty-client-sdk/.oxlintrc.json index 88218016ce..3b9c306a26 100644 --- a/packages/twenty-client-sdk/.oxlintrc.json +++ b/packages/twenty-client-sdk/.oxlintrc.json @@ -4,7 +4,7 @@ "categories": { "correctness": "off" }, - "ignorePatterns": ["node_modules", "dist", "generated"], + "ignorePatterns": ["node_modules", "dist", "generated", "src/generate/genql"], "rules": { "func-style": ["error", "declaration", { "allowArrowFunctions": true }], "no-console": "off", diff --git a/packages/twenty-client-sdk/.prettierignore b/packages/twenty-client-sdk/.prettierignore index 562ebe0503..bcb2540831 100644 --- a/packages/twenty-client-sdk/.prettierignore +++ b/packages/twenty-client-sdk/.prettierignore @@ -1,2 +1,3 @@ dist generated +src/generate/genql diff --git a/packages/twenty-client-sdk/package.json b/packages/twenty-client-sdk/package.json index f4848a0ed2..7c31a91504 100644 --- a/packages/twenty-client-sdk/package.json +++ b/packages/twenty-client-sdk/package.json @@ -48,11 +48,14 @@ "dist" ], "dependencies": { - "@genql/cli": "^3.0.3", + "@genql/runtime": "^2.10.0", "esbuild": "^0.28.0", - "graphql": "^16.8.1" + "graphql": "^16.8.1", + "lodash": "^4.17.21", + "prettier": "^2.8.8" }, "devDependencies": { + "@types/lodash": "^4.17.15", "@typescript/native-preview": "^7.0.0-dev.20260116.1", "tsc-alias": "^1.8.16", "twenty-shared": "workspace:*", diff --git a/packages/twenty-client-sdk/src/generate/__tests__/generated-client-query.test.ts b/packages/twenty-client-sdk/src/generate/__tests__/generated-client-query.test.ts new file mode 100644 index 0000000000..c2c0c73786 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/__tests__/generated-client-query.test.ts @@ -0,0 +1,156 @@ +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; + +import { generateCoreClientFromSchema } from '../generate-core-client'; + +// Exercises a client produced by the vendored genql codegen end to end: the +// real runtime builds the GraphQL operation from a selection, posts it through +// an injected fetch, and parses the response. This complements +// client-wrapper-auth.test.ts, which stubs createClient and so never runs the +// vendored runtime's query-building path. +const SCHEMA = ` +type Query { + hello: String + person(id: ID!): Person +} + +type Mutation { + createPerson(name: String!): Person +} + +type Person { + id: ID! + name: String +} + +schema { + query: Query + mutation: Mutation +} +`; + +type GeneratedClient = { + query: (request: Record) => Promise; + mutation: (request: Record) => Promise; +}; + +type CreateClient = (options: { + url?: string; + fetch?: typeof globalThis.fetch; + batch?: boolean; +}) => GeneratedClient; + +const jsonResponse = (body: unknown, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }); + +describe('Generated client queries the API (vendored genql runtime)', () => { + let temporaryDir: string; + let createClient: CreateClient; + + beforeAll(async () => { + temporaryDir = await mkdtemp(join(tmpdir(), 'twenty-genql-query-')); + const outputPath = join(temporaryDir, 'client'); + + await generateCoreClientFromSchema({ schema: SCHEMA, outputPath }); + + const generatedModule = await import( + `${pathToFileURL(join(outputPath, 'index.mjs')).href}?t=${Date.now()}` + ); + createClient = generatedModule.createClient as CreateClient; + }, 60000); + + afterAll(async () => { + if (temporaryDir) { + await rm(temporaryDir, { recursive: true, force: true }); + } + }); + + it('builds a GraphQL query from a selection and returns parsed data', async () => { + const fetchMock = vi.fn(async (_url: string, _init?: RequestInit) => + jsonResponse({ data: { person: { id: '1', name: 'Ada' } } }), + ); + + const client = createClient({ + url: 'https://example.test/graphql', + fetch: fetchMock as unknown as typeof globalThis.fetch, + }); + + const result = await client.query({ + person: { __args: { id: '1' }, id: true, name: true }, + }); + + expect(result).toEqual({ person: { id: '1', name: 'Ada' } }); + + expect(fetchMock).toHaveBeenCalledTimes(1); + const [calledUrl, requestInit] = fetchMock.mock.calls[0]; + expect(calledUrl).toBe('https://example.test/graphql'); + expect(requestInit?.method).toBe('POST'); + + const body = JSON.parse(String(requestInit?.body)); + expect(body.query).toMatch(/person/); + expect(body.query).toMatch(/\bid\b/); + expect(body.query).toMatch(/\bname\b/); + // the id argument made it into the operation (inline or as a variable) + expect(JSON.stringify(body)).toContain('1'); + }); + + it('builds a mutation operation', async () => { + const fetchMock = vi.fn(async (_url: string, _init?: RequestInit) => + jsonResponse({ data: { createPerson: { id: '2', name: 'Lin' } } }), + ); + + const client = createClient({ + url: 'https://example.test/graphql', + fetch: fetchMock as unknown as typeof globalThis.fetch, + }); + + const result = await client.mutation({ + createPerson: { __args: { name: 'Lin' }, id: true, name: true }, + }); + + expect(result).toEqual({ createPerson: { id: '2', name: 'Lin' } }); + const body = JSON.parse(String(fetchMock.mock.calls[0][1]?.body)); + expect(body.query).toMatch(/mutation/); + expect(body.query).toMatch(/createPerson/); + }); + + it('throws when the API responds with GraphQL errors', async () => { + const fetchMock = vi.fn(async () => + jsonResponse({ errors: [{ message: 'Not authorized' }], data: null }), + ); + + const client = createClient({ + url: 'https://example.test/graphql', + fetch: fetchMock as unknown as typeof globalThis.fetch, + }); + + await expect( + client.query({ person: { __args: { id: '1' }, id: true } }), + ).rejects.toThrow(); + }); + + it('rejects (does not hang) when a batched request fails', async () => { + const fetchMock = vi.fn(async () => { + throw new Error('network down'); + }); + + const client = createClient({ + url: 'https://example.test/graphql', + fetch: fetchMock as unknown as typeof globalThis.fetch, + // batch is the path where the runtime previously swallowed fetch errors, + // leaving the caller's promise unsettled. + batch: true, + }); + + await expect( + client.query({ person: { __args: { id: '1' }, id: true } }), + ).rejects.toThrow(/network down/); + }); +}); diff --git a/packages/twenty-client-sdk/src/generate/generate-core-client.ts b/packages/twenty-client-sdk/src/generate/generate-core-client.ts index c2d46e2c24..b293701f4c 100644 --- a/packages/twenty-client-sdk/src/generate/generate-core-client.ts +++ b/packages/twenty-client-sdk/src/generate/generate-core-client.ts @@ -1,12 +1,12 @@ import { appendFile, copyFile, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { generate } from '@genql/cli'; import { build } from 'esbuild'; import { DEFAULT_API_URL_NAME } from 'twenty-shared/application'; import { buildClientWrapperSource } from './client-wrapper'; import { emptyDir, ensureDir, move, remove } from './fs-utils'; +import { generate } from './genql'; import twentyClientTemplateSource from './twenty-client-template.ts?raw'; const COMMON_SCALAR_TYPES = { diff --git a/packages/twenty-client-sdk/src/generate/generate-metadata-client.ts b/packages/twenty-client-sdk/src/generate/generate-metadata-client.ts index 9e3d0de3f8..833350476b 100644 --- a/packages/twenty-client-sdk/src/generate/generate-metadata-client.ts +++ b/packages/twenty-client-sdk/src/generate/generate-metadata-client.ts @@ -1,11 +1,11 @@ import { appendFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { generate } from '@genql/cli'; import { DEFAULT_API_URL_NAME } from 'twenty-shared/application'; import { buildClientWrapperSource } from './client-wrapper'; import { emptyDir, ensureDir } from './fs-utils'; +import { generate } from './genql'; import twentyClientTemplateSource from './twenty-client-template.ts?raw'; const COMMON_SCALAR_TYPES = { diff --git a/packages/twenty-client-sdk/src/generate/genql/LICENSE b/packages/twenty-client-sdk/src/generate/genql/LICENSE new file mode 100644 index 0000000000..a3ad0c959a --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Tommaso De Rossi, morse + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/twenty-client-sdk/src/generate/genql/README.md b/packages/twenty-client-sdk/src/generate/genql/README.md new file mode 100644 index 0000000000..7a37181aa9 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/README.md @@ -0,0 +1,45 @@ +# Vendored genql codegen + +This folder is a narrowed, vendored copy of [`@genql/cli`](https://github.com/remorses/genql) +`3.0.5` (MIT, © Tommaso De Rossi "morse"), used to generate the typed GraphQL +client from an SDL string. + +It was vendored to remove `@genql/cli` from the dependency graph, which pulled +in abandoned and vulnerable transitive packages (`undici`, `native-fetch`, +`listr`, etc.) that Twenty never executed. + +## What was kept + +- `render/` — the schema → TypeScript client renderers (copied verbatim). +- `runtime/` — the genql client runtime, copied verbatim into every generated + client's `runtime/` folder (see `runtime-templates.ts`). +- `tasks/`, `helpers/`, `main.ts` — narrowed orchestration. + +## What was changed vs upstream + +- **Dropped the live-endpoint introspection path** (`schema/fetchSchema.ts`), + which was the only consumer of `undici` / `native-fetch` / `qs`. Twenty always + passes a schema string, never an endpoint. +- **Dropped `listr`** — the generation tasks now run as plain sequential + `async` functions. File contents are unchanged. +- **Replaced `fs-extra` / `mkdirp` / `rimraf`** with `node:fs`. +- **Replaced `@graphql-tools/load`** with graphql's own `buildSchema` — Twenty + passes an SDL string, so the extra loader (and its dependency) is unnecessary. + Verified to produce byte-identical output. +- **Runtime files are imported as `?raw` text** (`runtime-templates.ts`) instead + of read from `node_modules` at generation time, so they ship with this bundle. +- **`Config` was narrowed** to the schema-string inputs Twenty actually passes + (`schema`, `output`, `scalarTypes`, `sortProperties`). The introspection + (`endpoint`/`useGet`/`headers`), custom-`fetch` (`fetchImport`) and listr + (`verbose`) options were removed. The renderers are otherwise verbatim, so the + generated client still defaults its url/fetch to `undefined` (Twenty's wrapper + supplies them) and the output is unchanged. + +The generated output is byte-for-byte identical to what `@genql/cli@3.0.5` +produced; `prettier@^2.8` is retained for that reason. The runtime query path is +covered by `__tests__/generated-client-query.test.ts`, which drives a real +generated client against a mock transport. + +## License + +MIT — see `LICENSE`. diff --git a/packages/twenty-client-sdk/src/generate/genql/config.ts b/packages/twenty-client-sdk/src/generate/genql/config.ts new file mode 100644 index 0000000000..9dc46a47a4 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/config.ts @@ -0,0 +1,19 @@ +// Path the generated client imports its runtime from. +export const RUNTIME_LIB_NAME = './runtime'; + +// Configuration for the narrowed codegen. Twenty only ever generates from a +// schema string. Upstream genql additionally supported live-endpoint +// introspection (`endpoint`/`useGet`/`headers`), a custom `fetch` import +// (`fetchImport`) and listr `verbose` output — all of which were dropped when +// this codegen was vendored, so they are omitted here. The generated client's +// connection (url/fetch) is provided by the wrapper in twenty-client-template.ts. +export interface Config { + // the schema string (SDL) + schema?: string; + // the output dir + output?: string; + // maps GraphQL scalars to TypeScript types + scalarTypes?: { [k: string]: string }; + // sort the schema lexicographically before rendering + sortProperties?: boolean; +} diff --git a/packages/twenty-client-sdk/src/generate/genql/helpers/files.ts b/packages/twenty-client-sdk/src/generate/genql/helpers/files.ts new file mode 100644 index 0000000000..6b0c1e0afd --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/helpers/files.ts @@ -0,0 +1,36 @@ +import { promises as fs } from 'node:fs'; +import { homedir } from 'node:os'; +import { parse, resolve } from 'node:path'; + +// Guard against `clear` ever wiping something significant if a caller passes a +// misconfigured output path: refuse the filesystem root, shallow top-level +// directories, and the user's home directory or any of its ancestors. +const assertSafeToClear = (target: string) => { + const { root } = parse(target); + const depthBelowRoot = target + .slice(root.length) + .split(/[\\/]/) + .filter(Boolean).length; + + const home = resolve(homedir()); + const isHomeOrAncestor = target === home || home.startsWith(`${target}/`); + + if (target === root || depthBelowRoot < 2 || isHomeOrAncestor) { + throw new Error(`Refusing to recursively clear unsafe path: ${target}`); + } +}; + +export const ensurePath = async (path: string[], clear: boolean = false) => { + const target = resolve(...path); + if (clear) { + assertSafeToClear(target); + await fs.rm(target, { recursive: true, force: true }); + } + await fs.mkdir(target, { recursive: true }); +}; + +export const writeFileToPath = async (path: string[], content: string) => { + const folder = resolve(...path, '..'); + await fs.mkdir(folder, { recursive: true }); + await fs.writeFile(resolve(...path), content); +}; diff --git a/packages/twenty-client-sdk/src/generate/genql/helpers/prettify.ts b/packages/twenty-client-sdk/src/generate/genql/helpers/prettify.ts new file mode 100644 index 0000000000..010e824b89 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/helpers/prettify.ts @@ -0,0 +1,17 @@ +// @ts-nocheck +import prettier from 'prettier/standalone' +import { BuiltInParserName } from 'prettier' +import parserGraphql from 'prettier/parser-graphql' +import parserTS from 'prettier/parser-typescript' + +export const prettify = (code: string, parser?: BuiltInParserName): string => { + // return code + return prettier.format(code, { + parser, + plugins: [parserGraphql, parserTS], + semi: false, + singleQuote: true, + trailingComma: 'all', + printWidth: 80, + }) +} diff --git a/packages/twenty-client-sdk/src/generate/genql/index.ts b/packages/twenty-client-sdk/src/generate/genql/index.ts new file mode 100644 index 0000000000..07fa1ebfd2 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/index.ts @@ -0,0 +1,2 @@ +export { generate } from './main'; +export { type Config } from './config'; diff --git a/packages/twenty-client-sdk/src/generate/genql/main.ts b/packages/twenty-client-sdk/src/generate/genql/main.ts new file mode 100644 index 0000000000..9f369070d7 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/main.ts @@ -0,0 +1,14 @@ +import { type Config } from './config'; +import { writeClientFiles } from './tasks/clientTasks'; +import { loadConfiguredSchema } from './tasks/schemaTask'; + +// Drop-in replacement for `@genql/cli`'s `generate`, narrowed to the +// schema-string code path Twenty uses. See ./README.md for details. +export const generate = async (config: Config): Promise => { + if (!config.output) { + throw new Error('`output` must be defined in the config'); + } + + const schema = await loadConfiguredSchema(config); + await writeClientFiles(config, schema); +}; diff --git a/packages/twenty-client-sdk/src/generate/genql/render/client/renderClient.ts b/packages/twenty-client-sdk/src/generate/genql/render/client/renderClient.ts new file mode 100644 index 0000000000..f85e6ee442 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/client/renderClient.ts @@ -0,0 +1,158 @@ +// @ts-nocheck +import { GraphQLSchema } from 'graphql' +import { RenderContext } from '../common/RenderContext' +import { RUNTIME_LIB_NAME } from '../../config' +import { requestTypeName } from '../requestTypes/requestTypeName' + +const renderClientCode = (ctx: RenderContext) => { + const url = ctx.config?.endpoint ? `"${ctx.config.endpoint}"` : 'undefined' + const fetchImport = ctx.config?.fetchImport + return ` +function(options${url ? '?' : ''}: ClientOptions): Client { + return createClientOriginal({ + url: ${url}, + ${fetchImport ? `fetch,` : ''} + ...options, + queryRoot: typeMap.Query!, + mutationRoot: typeMap.Mutation!, + subscriptionRoot: typeMap.Subscription!, + }) as any +}` +} + +export const renderClientEsm = (schema: GraphQLSchema, ctx: RenderContext) => { + const queryType = schema.getQueryType() + const mutationType = schema.getMutationType() + const subscriptionType = schema.getSubscriptionType() + const fetchImport = ctx.config?.fetchImport || '' + ctx.addCodeBlock(` +${fetchImport} +${renderClientTypesImports({ mutationType, queryType, subscriptionType })} + import { + linkTypeMap, + createClient as createClientOriginal, + generateGraphqlOperation, + type FieldsSelection, type GraphqlOperation, type ClientOptions, GenqlError + } from '${RUNTIME_LIB_NAME}' + export type { FieldsSelection } from '${RUNTIME_LIB_NAME}' + export { GenqlError } + + import types from './types' + export * from './schema' + const typeMap = linkTypeMap(types as any) + + ${renderClientType({ mutationType, queryType, subscriptionType })} + + export const createClient = ${renderClientCode(ctx)} + + export const everything = { + __scalar: true + } + `) + + if (queryType) { + ctx.addCodeBlock(` + export type QueryResult = FieldsSelection<${queryType.name}, fields> + export const generateQueryOp: (fields: ${requestTypeName( + queryType, + )} & { __name?: string }) => GraphqlOperation = function(fields) { + return generateGraphqlOperation('query', typeMap.Query!, fields as any) + } + `) + } + if (mutationType) { + ctx.addCodeBlock(` + export type MutationResult = FieldsSelection<${mutationType.name}, fields> + export const generateMutationOp: (fields: ${requestTypeName( + mutationType, + )} & { __name?: string }) => GraphqlOperation = function(fields) { + return generateGraphqlOperation('mutation', typeMap.Mutation!, fields as any) + } + `) + } + if (subscriptionType) { + ctx.addCodeBlock(` + export type SubscriptionResult = FieldsSelection<${subscriptionType.name}, fields> + export const generateSubscriptionOp: (fields: ${requestTypeName( + subscriptionType, + )} & { __name?: string }) => GraphqlOperation = function(fields) { + return generateGraphqlOperation('subscription', typeMap.Subscription!, fields as any) + } + `) + } +} + +function renderClientTypesImports({ + queryType, + mutationType, + subscriptionType, +}) { + const imports: string[] = [] + if (queryType) { + imports.push( + requestTypeName(queryType), + + queryType.name, + ) + } + + if (mutationType) { + imports.push( + requestTypeName(mutationType), + + mutationType.name, + ) + } + if (subscriptionType) { + imports.push( + requestTypeName(subscriptionType), + + subscriptionType.name, + ) + } + if (imports.length > 0) { + return `import type {${imports.join(',')}} from './schema'` + } + return '' +} + +function renderClientType({ queryType, mutationType, subscriptionType }) { + let interfaceContent = '' + + if (queryType) { + interfaceContent += ` + query( + request: R & { __name?: string }, + ): Promise> + ` + } + + if (mutationType) { + interfaceContent += ` + mutation( + request: R & { __name?: string }, + ): Promise> + ` + } + + // TODO add subscription client again + // if (subscriptionType) { + // interfaceContent += ` + // subscription( + // request: R & { __name?: string }, + // ): Observable> + // ` + // } + + return ` + export interface Client { + ${interfaceContent} + } + ` +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/common/RenderContext.ts b/packages/twenty-client-sdk/src/generate/genql/render/common/RenderContext.ts new file mode 100644 index 0000000000..7a104545ca --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/common/RenderContext.ts @@ -0,0 +1,107 @@ +// @ts-nocheck +import { GraphQLSchema } from 'graphql' +import { BuiltInParserName } from 'prettier' +import { Config } from '../../config' +import { prettify } from '../../helpers/prettify' +import { relativeImportPath } from './relativeImportPath' + +interface Import { + isDefault: boolean + module?: string + alias?: string +} + +interface ImportMap { + [from: string]: Import[] +} + +export class RenderContext { + protected codeBlocks: string[] = [] + protected imports: ImportMap = {} + protected importAliasCounter = 0 + + constructor(public schema?: GraphQLSchema, public config?: Config) {} + + addCodeBlock(block: string) { + if (block) { + this.codeBlocks.push(block) + } + } + + addImport( + from: string, + isDefault: boolean, + module?: string, + fromAbsolute?: boolean, + noAlias?: boolean, + ) { + if (this.config && this.config.output) { + from = fromAbsolute + ? from + : relativeImportPath(this.config.output, from) + } + + if (!this.imports[from]) this.imports[from] = [] + + const imports = this.imports[from] + + const existing = imports.find( + (i) => + (isDefault && i.isDefault) || + (!isDefault && i.module === module), + ) + + if (existing) return existing.alias + + this.importAliasCounter++ + const alias = noAlias ? undefined : `a${this.importAliasCounter}` + imports.push({ isDefault, module, alias }) + + return alias + } + + protected getImportBlock() { + const imports: string[] = [] + + Object.keys(this.imports).forEach((from) => { + let defaultImport = this.imports[from].find((i) => i.isDefault) + const namedImports = this.imports[from].filter((i) => !i.isDefault) + const statements: string[] = [] + + if (defaultImport) { + statements.push(defaultImport.alias || '') + } + + if (namedImports.length > 0) { + statements.push( + `{${namedImports + .map((i) => + i.alias ? `${i.module} as ${i.alias}` : i.module, + ) + .join(',')}}`, + ) + } + + imports.push(`import ${statements.join(',')} from '${from}'`) + }) + + if (imports.length > 0) return imports.join('\n') + else return + } + + toCode(parser?: BuiltInParserName, pretty = false) { + const blocks = [...this.codeBlocks] + + if (parser && (parser === 'typescript' || parser === 'babel')) { + const importBlock = this.getImportBlock() + if (importBlock) blocks.unshift(importBlock) + } + if (parser && pretty) { + return prettify(blocks.join('\n\n'), parser) + } + if (parser) { + return blocks.join('\n\n') + } + return blocks.join('') + } +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/common/comment.ts b/packages/twenty-client-sdk/src/generate/genql/render/common/comment.ts new file mode 100644 index 0000000000..fdab00b40a --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/common/comment.ts @@ -0,0 +1,37 @@ +// @ts-nocheck +import { GraphQLArgument, GraphQLEnumValue, GraphQLField, GraphQLInputField, GraphQLNamedType } from 'graphql' + + +export const comment = (comment: { text?: string | null; deprecated?: string | null }) => { + const lines: string[] = [] + + if (comment.deprecated) { + lines.push(`@deprecated ${comment.deprecated.replace(/\s/g, ' ')}`) + } + + if (comment.text) { + lines.push(...comment.text.split('\n')) + } + + return lines.length > 0 + ? lines.length === 1 + ? `\n/** ${lines[0]} */\n` + : `\n/**\n${lines.map(l => ` * ${l}`).join('\n')}\n */\n` + : '' +} + +export const typeComment = (type: GraphQLNamedType) => + comment({ + text: type.description, + }) + +export const fieldComment = (field: GraphQLEnumValue | GraphQLField) => + comment({ + deprecated: field.deprecationReason, + text: field.description, + }) + +export const argumentComment = (arg: GraphQLArgument | GraphQLInputField) => + comment({ + text: arg.description, + }) diff --git a/packages/twenty-client-sdk/src/generate/genql/render/common/excludedTypes.ts b/packages/twenty-client-sdk/src/generate/genql/render/common/excludedTypes.ts new file mode 100644 index 0000000000..de9386be5d --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/common/excludedTypes.ts @@ -0,0 +1,11 @@ +// @ts-nocheck +export const excludedTypes = [ + '__Schema', + '__Type', + '__TypeKind', + '__Field', + '__InputValue', + '__EnumValue', + '__Directive', + '__DirectiveLocation', +] diff --git a/packages/twenty-client-sdk/src/generate/genql/render/common/relativeImportPath.ts b/packages/twenty-client-sdk/src/generate/genql/render/common/relativeImportPath.ts new file mode 100644 index 0000000000..d22c54d825 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/common/relativeImportPath.ts @@ -0,0 +1,7 @@ +// @ts-nocheck +import path from 'path' + +export const relativeImportPath = (from: string, to: string) => { + const fromResolved = path.relative(from, to) + return fromResolved[0] === '.' ? fromResolved : `./${fromResolved}` +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/common/renderTyping.ts b/packages/twenty-client-sdk/src/generate/genql/render/common/renderTyping.ts new file mode 100644 index 0000000000..cb79b76327 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/common/renderTyping.ts @@ -0,0 +1,63 @@ +// @ts-nocheck +import { GraphQLInputType, GraphQLNonNull, GraphQLOutputType, isListType, isNamedType, isNonNullType, isScalarType } from 'graphql' + + +const render = ( + type: GraphQLOutputType | GraphQLInputType, + nonNull: boolean, + root: boolean, + undefinableValues: boolean, + undefinableFields: boolean, + wrap: (x: string) => string = x => x +): string => { + + if (root) { + if (undefinableFields) { + if (isNonNullType(type)) { + return `: ${render(type.ofType, true, false, undefinableValues, undefinableFields, wrap)}` + } else { + const rendered = render(type, true, false, undefinableValues, undefinableFields, wrap) + return undefinableValues ? `?: ${rendered}` : `?: (${rendered} | null)` + } + } else { + return `: ${render(type, false, false, undefinableValues, undefinableFields, wrap)}` + } + } + + if (isNamedType(type)) { + let typeName = type.name + + // if is a scalar use the scalar interface to not expose reserved words + if (isScalarType(type)) { + typeName = `Scalars['${typeName}']` + } + + const typing = wrap(typeName) + + if (undefinableValues) { + return nonNull ? typing : `(${typing} | undefined)` + } else { + return nonNull ? typing : `(${typing} | null)` + } + } + + if (isListType(type)) { + const typing = `${render(type.ofType, false, false, undefinableValues, undefinableFields, wrap)}[]` + + if (undefinableValues) { + return nonNull ? typing : `(${typing} | undefined)` + } else { + return nonNull ? typing : `(${typing} | null)` + } + } + + return render((type as GraphQLNonNull).ofType, true, false, undefinableValues, undefinableFields, wrap) +} + +export const renderTyping = ( + type: GraphQLOutputType | GraphQLInputType, + undefinableValues: boolean, + undefinableFields: boolean, + root = true, + wrap: any = undefined +) => render(type, false, root, undefinableValues, undefinableFields, wrap) diff --git a/packages/twenty-client-sdk/src/generate/genql/render/common/support.ts b/packages/twenty-client-sdk/src/generate/genql/render/common/support.ts new file mode 100644 index 0000000000..f7cf82db62 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/common/support.ts @@ -0,0 +1,16 @@ +// @ts-nocheck +export function sortKeys(obj: Record): Record { + obj = obj || {} + const ordered = {} + Object.keys(obj) + .sort() + // .reverse() + .forEach(function(key) { + ordered[key] = obj[key] + }) + return ordered +} + +export function intersection(a: T[][]): T[] { + return a.reduce((p, c) => p.filter((e) => c.includes(e))) +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/requestTypes/inputObjectType.ts b/packages/twenty-client-sdk/src/generate/genql/render/requestTypes/inputObjectType.ts new file mode 100644 index 0000000000..588f686748 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/requestTypes/inputObjectType.ts @@ -0,0 +1,22 @@ +// @ts-nocheck +import { GraphQLInputObjectType } from 'graphql' +import { argumentComment, typeComment } from '../common/comment' +import { RenderContext } from '../common/RenderContext' +import { renderTyping } from '../common/renderTyping' +import { sortKeys } from '../common/support' + +export const inputObjectType = (type: GraphQLInputObjectType, ctx: RenderContext) => { + let fields = type.getFields() + + if (ctx.config?.sortProperties) { + fields = sortKeys(fields) + } + + + const fieldStrings = Object.keys(fields).map(fieldName => { + const field = fields[fieldName] + return `${argumentComment(field)}${field.name}${renderTyping(field.type, false, true)}` + }) + + ctx.addCodeBlock(`${typeComment(type)}export interface ${type.name} {${fieldStrings.join(',')}}`) +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/requestTypes/objectType.ts b/packages/twenty-client-sdk/src/generate/genql/render/requestTypes/objectType.ts new file mode 100644 index 0000000000..13eefbe545 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/requestTypes/objectType.ts @@ -0,0 +1,111 @@ +// @ts-nocheck +import { + getNamedType, + GraphQLField, + GraphQLInterfaceType, + GraphQLObjectType, + isEnumType, + isInterfaceType, + isScalarType, +} from 'graphql' +import { argumentComment, fieldComment, typeComment } from '../common/comment' +import { RenderContext } from '../common/RenderContext' + +import { requestTypeName } from './requestTypeName' +import { sortKeys } from '../common/support' +import { renderTyping } from '../common/renderTyping' + +const INDENTATION = ' ' + +export const objectType = ( + type: GraphQLObjectType | GraphQLInterfaceType, + ctx: RenderContext, +) => { + let fields = type.getFields() + + if (ctx.config?.sortProperties) { + fields = sortKeys(fields) + } + + let fieldStrings = Object.keys(fields).map((fieldName) => { + const field = fields[fieldName] + + const types: string[] = [] + const resolvedType = getNamedType(field.type) + const resolvable = !( + isEnumType(resolvedType) || isScalarType(resolvedType) + ) + const argsPresent = field.args.length > 0 + const argsString = toArgsString(field) + const argsOptional = !argsString.match(/[^?]:/) + + if (argsPresent) { + if (resolvable) { + types.push( + `(${requestTypeName(resolvedType)} & { __args${ + argsOptional ? '?' : '' + }: ${argsString} })`, + ) + } else { + // TODO if i want to add __directive support, i need to make this __args optional + types.push(`{ __args: ${argsString} }`) + } + // if (resolvable) { + // types.push(`[${argsString},${requestTypeName(resolvedType)}]`) + // } else { + // types.push(`[${argsString}]`) + // } + } + + if (argsOptional && !resolvable) { + types.push('boolean | number') + } + if (!argsPresent && resolvable) { + types.push(requestTypeName(resolvedType)) + } + + return `${fieldComment(field)}${field.name}?: ${types.join(' | ')}` + }) + + if (isInterfaceType(type) && ctx.schema) { + let interfaceProperties = ctx.schema + .getPossibleTypes(type) + .map((t) => `on_${t.name}?: ${requestTypeName(t)}`) + if (ctx.config?.sortProperties) { + interfaceProperties = interfaceProperties.sort() + } + fieldStrings = fieldStrings.concat(interfaceProperties) + } + + fieldStrings.push('__typename?: boolean | number') + fieldStrings.push('__scalar?: boolean | number') + + // add indentation + fieldStrings = fieldStrings.map((x) => + x + .split('\n') + .filter(Boolean) + .map((l) => INDENTATION + l) + .join('\n'), + ) + + ctx.addCodeBlock( + `${typeComment(type)}export interface ${requestTypeName( + type, + )}{\n${fieldStrings.join('\n')}\n}`, + ) +} + +export const toArgsString = (field: GraphQLField) => { + let fields = field.args + .map( + (a) => + `${argumentComment(a)}${a.name}${renderTyping( + a.type, + false, + true, + )}`, + ) + .join(', ') + return `{${fields}}` +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/requestTypes/renderRequestTypes.ts b/packages/twenty-client-sdk/src/generate/genql/render/requestTypes/renderRequestTypes.ts new file mode 100644 index 0000000000..90d98a3f1d --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/requestTypes/renderRequestTypes.ts @@ -0,0 +1,65 @@ +// @ts-nocheck +import { + GraphQLSchema, + isInputObjectType, + isInterfaceType, + isObjectType, + isUnionType, + GraphQLObjectType, +} from 'graphql' +import { excludedTypes } from '../common/excludedTypes' +import { RenderContext } from '../common/RenderContext' +import { inputObjectType } from './inputObjectType' +import { objectType } from './objectType' +import { unionType } from './unionType' +import { sortKeys } from '../common/support' +import { requestTypeName } from './requestTypeName' + +export const renderRequestTypes = ( + schema: GraphQLSchema, + ctx: RenderContext, +) => { + let typeMap = schema.getTypeMap() + + if (ctx.config?.sortProperties) { + typeMap = sortKeys(typeMap) + } + + for (const name in typeMap) { + if (excludedTypes.includes(name)) continue + + const type = typeMap[name] + + if (isObjectType(type) || isInterfaceType(type)) objectType(type, ctx) + if (isInputObjectType(type)) inputObjectType(type, ctx) + if (isUnionType(type)) unionType(type, ctx) + } + + const aliases = [ + { type: schema.getQueryType(), name: 'QueryGenqlSelection' }, + { type: schema.getMutationType(), name: 'MutationGenqlSelection' }, + { + type: schema.getSubscriptionType(), + name: 'SubscriptionGenqlSelection', + }, + ] + .map(renderAlias) + .filter(Boolean) + .join('\n') + + ctx.addCodeBlock(aliases) +} + +function renderAlias({ + type, + name, +}: { + type?: GraphQLObjectType | null + name: string +}) { + if (type && requestTypeName(type) !== name) { + // TODO make the camel case or kebab case an option + return `export type ${name} = ${requestTypeName(type)}` + } + return '' +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/requestTypes/requestTypeName.ts b/packages/twenty-client-sdk/src/generate/genql/render/requestTypes/requestTypeName.ts new file mode 100644 index 0000000000..db9152a14d --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/requestTypes/requestTypeName.ts @@ -0,0 +1,7 @@ +// @ts-nocheck +import { GraphQLNamedType } from 'graphql' + +export const requestTypeName = (type?: GraphQLNamedType) => { + if (!type) return '' + return `${type.name}GenqlSelection` +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/requestTypes/unionType.ts b/packages/twenty-client-sdk/src/generate/genql/render/requestTypes/unionType.ts new file mode 100644 index 0000000000..d871b66d62 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/requestTypes/unionType.ts @@ -0,0 +1,29 @@ +// @ts-nocheck +import { GraphQLUnionType } from 'graphql' +import uniq from 'lodash/uniq' +import { typeComment } from '../common/comment' +import { RenderContext } from '../common/RenderContext' +import { requestTypeName } from './requestTypeName' + +export const unionType = (type: GraphQLUnionType, ctx: RenderContext) => { + let types = [...type.getTypes()] + if (ctx.config?.sortProperties) { + types = types.sort() + } + const fieldStrings = types.map((t) => `on_${t.name}?:${requestTypeName(t)}`) + + const commonInterfaces = uniq(types.map((x) => x.getInterfaces()).flat()) + fieldStrings.push( + ...commonInterfaces.map((type) => { + return `on_${type.name}?: ${requestTypeName(type)}` + }), + ) + + fieldStrings.push('__typename?: boolean | number') + + ctx.addCodeBlock( + `${typeComment(type)}export interface ${requestTypeName( + type, + )}{\n${fieldStrings.map((x) => ' ' + x).join(',\n')}\n}`, + ) +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/enumType.ts b/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/enumType.ts new file mode 100644 index 0000000000..910094d3ee --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/enumType.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { GraphQLEnumType } from 'graphql' +import { typeComment } from '../common/comment' +import { RenderContext } from '../common/RenderContext' + +export const enumType = (type: GraphQLEnumType, ctx: RenderContext) => { + const values = type.getValues().map(v => `'${v.name}'`) + ctx.addCodeBlock(`${typeComment(type)}export type ${type.name} = ${values.join(' | ')}`) +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/interfaceType.ts b/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/interfaceType.ts new file mode 100644 index 0000000000..f13d1e0bfc --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/interfaceType.ts @@ -0,0 +1,56 @@ +// @ts-nocheck +import { GraphQLInterfaceType } from 'graphql' +import { RenderContext } from '../common/RenderContext' +import { typeComment } from '../common/comment' +import { objectType } from './objectType' + +export const interfaceType = ( + type: GraphQLInterfaceType, + ctx: RenderContext, +) => { + if (!ctx.schema) { + throw new Error('schema is required to render unionType') + } + const typeNames = ctx.schema.getPossibleTypes(type).map((t) => t.name) + if (!typeNames.length) { + objectType(type, ctx) + } else { + ctx.addCodeBlock( + `${typeComment(type)}export type ${type.name} = (${typeNames.join( + ' | ', + )}) & { __isUnion?: true }`, + ) + } +} + +// interface should produce an object like +// export type Nameable = { +// __interface:{ +// name:string +// }; +// __resolve:{ +// ['on_Card']: Card; +// ['on_CardStack']: CardStack; +// } +// } + +// export const interfaceType = (type: GraphQLInterfaceType, ctx: RenderContext) => { +// if (!ctx.schema) { +// throw new Error('schema is req required to render unionType ') +// } +// const typeNames = ctx.schema.getPossibleTypes(type).map((t) => t.name) +// let resolveContent = typeNames +// .map((name) => `on_${name}?: ${name}`) +// .join('\n ') + +// ctx.addCodeBlock( +// `${typeComment(type)}export type ${type.name}={ +// __interface: +// ${typeNames.join('|')} +// __resolve: { +// ${resolveContent} +// } +// __typename?: string +// }`, +// ) +// } diff --git a/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/objectType.ts b/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/objectType.ts new file mode 100644 index 0000000000..1d3df24c2b --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/objectType.ts @@ -0,0 +1,69 @@ +// @ts-nocheck +import { + GraphQLInterfaceType, + GraphQLObjectType, + isObjectType, +} from 'graphql' +import { fieldComment, typeComment } from '../common/comment' +import { RenderContext } from '../common/RenderContext' +import { renderTyping } from '../common/renderTyping' +import { sortKeys } from '../common/support' + +const INDENTATION = ' ' + +export const objectType = ( + type: GraphQLObjectType | GraphQLInterfaceType, + ctx: RenderContext, +) => { + let fieldsMap = type.getFields() + + if (ctx.config?.sortProperties) { + fieldsMap = sortKeys(fieldsMap) + } + + const fields = Object.keys(fieldsMap).map( + (fieldName) => fieldsMap[fieldName], + ) + + if (!ctx.schema) throw new Error('no schema provided') + + const typeNames = isObjectType(type) + ? [type.name] + : ctx.schema.getPossibleTypes(type).map((t) => t.name) + + let fieldStrings = fields + .map((f) => { + return `${fieldComment(f)}${f.name}${renderTyping( + f.type, + true, + true, + )}` + }) + .concat([ + `__typename: ${ + typeNames.length > 0 + ? typeNames.map((t) => `'${t}'`).join('|') + : 'string' + }`, + ]) + // add indentation + fieldStrings = fieldStrings.map((x) => + x + .split('\n') + .filter(Boolean) + .map((l) => INDENTATION + l) + .join('\n'), + ) + + // there is no need to add extensions as in graphql the implemented type must explicitly add the fields + // const interfaceNames = isObjectType(type) + // ? type.getInterfaces().map((i) => i.name) + // : [] + // let extensions = + // interfaceNames.length > 0 ? ` extends ${interfaceNames.join(',')}` : '' + ctx.addCodeBlock( + `${typeComment(type)}export interface ${ + type.name + } {\n${fieldStrings.join('\n')}\n}`, + ) +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/renderResponseTypes.ts b/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/renderResponseTypes.ts new file mode 100644 index 0000000000..e8cf9c0f2d --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/renderResponseTypes.ts @@ -0,0 +1,71 @@ +// @ts-nocheck +import { + GraphQLSchema, + isEnumType, + isInterfaceType, + isObjectType, + isScalarType, + isUnionType, + GraphQLScalarType, + GraphQLType, + GraphQLObjectType, +} from 'graphql' +import { excludedTypes } from '../common/excludedTypes' +import { RenderContext } from '../common/RenderContext' +import { enumType } from './enumType' +import { objectType } from './objectType' +import { renderScalarTypes } from './scalarType' +import { unionType } from './unionType' +import { interfaceType } from './interfaceType' +import { sortKeys } from '../common/support' + +export const renderResponseTypes = ( + schema: GraphQLSchema, + ctx: RenderContext, +) => { + let typeMap = schema.getTypeMap() + if (ctx.config?.sortProperties) { + typeMap = sortKeys(typeMap) + } + ctx.addCodeBlock( + renderScalarTypes( + ctx, + Object.values(typeMap).filter((type): type is GraphQLScalarType => + isScalarType(type), + ), + ), + ) + for (const name in typeMap) { + if (excludedTypes.includes(name)) continue + + const type = typeMap[name] + + if (isEnumType(type)) enumType(type, ctx) + if (isUnionType(type)) unionType(type, ctx) + if (isObjectType(type)) objectType(type, ctx) + if (isInterfaceType(type)) interfaceType(type, ctx) + } + + const aliases = [ + { type: schema.getQueryType(), name: 'Query' }, + { type: schema.getMutationType(), name: 'Mutation' }, + { type: schema.getSubscriptionType(), name: 'Subscription' }, + ] + .map(renderAlias) + .filter(Boolean) + .join('\n') + ctx.addCodeBlock(aliases) +} + +function renderAlias({ + type, + name, +}: { + type?: GraphQLObjectType | null + name: string +}) { + if (type && type.name !== name) { + return `export type ${name} = ${type.name}` + } + return '' +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/scalarType.ts b/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/scalarType.ts new file mode 100644 index 0000000000..7969bf711c --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/scalarType.ts @@ -0,0 +1,15 @@ +// @ts-nocheck +import { GraphQLScalarType } from 'graphql' +import { RenderContext } from '../common/RenderContext' +import { getTypeMappedAlias } from './typeMappedAlias' + +export function renderScalarTypes( + ctx: RenderContext, + types: GraphQLScalarType[], +) { + let content = '' + types.forEach((type) => { + content += ` ${type.name}: ${getTypeMappedAlias(type, ctx)},\n` + }) + return `export type Scalars = {\n${content}}` +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/typeMappedAlias.ts b/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/typeMappedAlias.ts new file mode 100644 index 0000000000..6c160c5113 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/typeMappedAlias.ts @@ -0,0 +1,33 @@ +// @ts-nocheck +import { GraphQLNamedType } from 'graphql' +import { RenderContext } from '../common/RenderContext' + +const knownTypes: { + [name: string]: string +} = { + Int: 'number', + Float: 'number', + String: 'string', + Boolean: 'boolean', + ID: 'string', +} + +export const getTypeMappedAlias = ( + type: GraphQLNamedType, + ctx: RenderContext, +) => { + const map = { ...knownTypes, ...(ctx?.config?.scalarTypes || {}) } + return map?.[type.name] || 'any' +} + +// export const renderTypeMappedAlias = ( +// type: GraphQLNamedType, +// ctx: RenderContext, +// ) => { +// const mappedType = getTypeMappedAlias(type, ctx) +// if (mappedType) { +// ctx.addCodeBlock( +// `${typeComment(type)}export type ${type.name} = ${mappedType}`, +// ) +// } +// } diff --git a/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/unionType.ts b/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/unionType.ts new file mode 100644 index 0000000000..831645401e --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/responseTypes/unionType.ts @@ -0,0 +1,43 @@ +// @ts-nocheck +import { GraphQLUnionType } from 'graphql' +import { RenderContext } from '../common/RenderContext' +import { typeComment } from '../common/comment' + +// union should produce an object like +// export type ChangeCard = { +// __union:SpecialCard | EffectCard; +// __resolve:{ +// ['...on SpecialCard']: SpecialCard; +// ['...on EffectCard']: EffectCard; +// } +// } + +export const unionType = (type: GraphQLUnionType, ctx: RenderContext) => { + let typeNames = type.getTypes().map((t) => t.name) + if (ctx.config?.sortProperties) { + typeNames = typeNames.sort() + } + ctx.addCodeBlock( + `${typeComment(type)}export type ${type.name} = (${typeNames.join( + ' | ', + )}) & { __isUnion?: true }`, + ) +} + +// export const unionType = (type: GraphQLUnionType, ctx: RenderContext) => { +// const typeNames = type.getTypes().map((t) => t.name) +// let resolveContent = typeNames +// .map((name) => `on_${name}?: ${name}`) +// .join('\n ') + +// ctx.addCodeBlock( +// `${typeComment(type)}export type ${type.name}={ +// __union: +// ${typeNames.join('|')} +// __resolve: { +// ${resolveContent} +// } +// __typename?: string +// }`, +// ) +// } diff --git a/packages/twenty-client-sdk/src/generate/genql/render/schema/renderSchema.ts b/packages/twenty-client-sdk/src/generate/genql/render/schema/renderSchema.ts new file mode 100644 index 0000000000..7a2e2ebb8a --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/schema/renderSchema.ts @@ -0,0 +1,7 @@ +// @ts-nocheck +import { GraphQLSchema, printSchema } from 'graphql' +import { RenderContext } from '../common/RenderContext' + +export const renderSchema = (schema: GraphQLSchema, ctx: RenderContext) => { + ctx.addCodeBlock(printSchema(schema)) +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/typeGuards/renderTypeGuards.ts b/packages/twenty-client-sdk/src/generate/genql/render/typeGuards/renderTypeGuards.ts new file mode 100644 index 0000000000..2af8640bc3 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/typeGuards/renderTypeGuards.ts @@ -0,0 +1,40 @@ +// @ts-nocheck +import { + GraphQLSchema, + isInterfaceType, + isObjectType, + isUnionType, +} from 'graphql' +import { excludedTypes } from '../common/excludedTypes' +import { RenderContext } from '../common/RenderContext' + +const renderTypeGuard = (target: string, possible: string[]) => { + return ` + const ${target}_possibleTypes: string[] = [${possible + .map((t) => `'${t}'`) + .join(',')}] + export const is${target} = (obj?: { __typename?: any } | null): obj is ${target} => { + if (!obj?.__typename) throw new Error('__typename is missing in "is${target}"') + return ${target}_possibleTypes.includes(obj.__typename) + } + ` +} + +export const renderTypeGuards = (schema: GraphQLSchema, ctx: RenderContext) => { + const typeMap = schema.getTypeMap() + for (const name in typeMap) { + if (excludedTypes.includes(name)) continue + + const type = typeMap[name] + + if (isUnionType(type)) { + const types = type.getTypes().map((t) => t.name) + ctx.addCodeBlock(renderTypeGuard(type.name, types)) + } else if (isInterfaceType(type)) { + const types = schema.getPossibleTypes(type).map((t) => t.name) + ctx.addCodeBlock(renderTypeGuard(type.name, types)) + } else if (isObjectType(type)) { + ctx.addCodeBlock(renderTypeGuard(type.name, [type.name])) + } + } +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/typeMap/objectType.ts b/packages/twenty-client-sdk/src/generate/genql/render/typeMap/objectType.ts new file mode 100644 index 0000000000..83e28a0711 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/typeMap/objectType.ts @@ -0,0 +1,67 @@ +// @ts-nocheck +import { + getNamedType, + GraphQLInterfaceType, + GraphQLObjectType, + isEnumType, + isInterfaceType, + isScalarType, + GraphQLInputObjectType, + GraphQLArgument, + GraphQLField, +} from 'graphql' +import { RenderContext } from '../common/RenderContext' +import { ArgMap, Field, FieldMap } from '../../runtime/types' +import { isEmpty } from './support' + +export const objectType = ( + type: GraphQLObjectType | GraphQLInterfaceType | GraphQLInputObjectType, + ctx: RenderContext, +) => { + const typeObj: FieldMap = Object.keys(type.getFields()).reduce< + FieldMap + >((r, f) => { + const field = type.getFields()[f] + const namedType = getNamedType(field.type) + const fieldObj: Field = { type: namedType.name } + r[f] = fieldObj + + const args: readonly GraphQLArgument[] = + (field as GraphQLField).args || [] + + if (args.length > 0) { + fieldObj.args = args.reduce>((r, a) => { + const concreteType = a.type.toString() + const typename = getNamedType(a.type).name + r[a.name] = [typename] + if (typename !== concreteType) { + r[a.name]?.push(concreteType) + } + return r + }, {}) + } + + return r + }, {}) + + if (isInterfaceType(type) && ctx.schema) { + ctx.schema.getPossibleTypes(type).map((t) => { + if (!isEmpty(typeObj)) { + typeObj[`on_${t.name}`] = { type: t.name } + } + }) + } + + if (!isEmpty(typeObj)) { + typeObj.__typename = { type: 'String' } + } + + // const scalar = Object.keys(type.getFields()) + // .map(f => type.getFields()[f]) + // .filter(f => isScalarType(getNamedType(f.type)) || isEnumType(getNamedType(f.type))) + // .map(f => f.name) + + // if (scalar.length > 0) typeObj.scalar = scalar + + return typeObj +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/typeMap/renderTypeMap.ts b/packages/twenty-client-sdk/src/generate/genql/render/typeMap/renderTypeMap.ts new file mode 100644 index 0000000000..43ac9821aa --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/typeMap/renderTypeMap.ts @@ -0,0 +1,140 @@ +// @ts-nocheck +import { + ArgMap, + CompressedField, + CompressedFieldMap, + CompressedTypeMap, + TypeMap, +} from '../../runtime/types' +import { + GraphQLSchema, + isEnumType, + isInputObjectType, + isInterfaceType, + isObjectType, + isScalarType, + isUnionType, +} from 'graphql' +import { excludedTypes } from '../common/excludedTypes' +import { RenderContext } from '../common/RenderContext' +import { objectType } from './objectType' +import { unionType } from './unionType' + +export const renderTypeMap = (schema: GraphQLSchema, ctx: RenderContext) => { + // remove fields key, + // remove the Type.type and Type.args, replace with [type, args] + // reverse args.{name} + // Args type is deduced and added only when the concrete type is different from type name, remove the scalar field and replace with a top level scalars array field. + const result: TypeMap = { + scalars: [], + types: {}, + } + + Object.keys(schema.getTypeMap()) + .filter((t) => !excludedTypes.includes(t)) + .map((t) => schema.getTypeMap()[t]) + .map((t) => { + if (isObjectType(t) || isInterfaceType(t) || isInputObjectType(t)) + result.types[t.name] = objectType(t, ctx) + else if (isUnionType(t)) result.types[t.name] = unionType(t, ctx) + else if (isScalarType(t) || isEnumType(t)) { + result.scalars.push(t.name) + result.types[t.name] = {} + } + }) + + // change names of query, mutation on schemas that chose different names (hasura) + const q = schema.getQueryType() + if (q?.name && q?.name !== 'Query') { + delete result.types[q.name] + result.types.Query = objectType(q, ctx) + // result.Query.name = 'Query' + } + + const m = schema.getMutationType() + if (m?.name && m.name !== 'Mutation') { + delete result.types[m.name] + result.types.Mutation = objectType(m, ctx) + // result.Mutation.name = 'Mutation' + } + + const s = schema.getSubscriptionType() + if (s?.name && s.name !== 'Subscription') { + delete result.types[s.name] + result.types.Subscription = objectType(s, ctx) + // result.Subscription.name = 'Subscription' + } + + ctx.addCodeBlock( + JSON.stringify(replaceTypeNamesWithIndexes(result), null, 4), + ) + + + +} + +export function replaceTypeNamesWithIndexes( + typeMap: TypeMap, +): CompressedTypeMap { + const nameToIndex: Record = Object.assign( + {}, + ...Object.keys(typeMap.types).map((k, i) => ({ [k]: i })), + ) + const scalars = typeMap.scalars.map((x) => nameToIndex[x]) + const types = Object.assign( + {}, + ...Object.keys(typeMap.types || {}).map((k) => { + const type = typeMap.types[k] + const fieldsMap = type || {} + // processFields(fields, indexToName) + const fields = Object.assign( + {}, + ...Object.keys(fieldsMap).map( + (f): CompressedFieldMap => { + const content = fieldsMap[f] as any + if (!content) { + throw new Error('no content in field ' + f) + } + const [typeName, args] = [content.type, content.args] + const res: CompressedField = [ + typeName ? nameToIndex[typeName] : -1, + ] + if (args) { + res[1] = Object.assign( + {}, + ...Object.keys(args || {}).map((k) => { + const arg = args?.[k] + if (!arg) { + throw new Error( + 'replaceTypeNamesWithIndexes: no arg for ' + + k, + ) + } + return { + [k]: [ + nameToIndex[arg[0]], + ...arg.slice(1), + ], + } as ArgMap + }), + ) + } + + return { + [f]: res, + } + }, + ), + ) + return { + [k]: { + ...fields, + }, + } + }), + ) + return { + scalars, + types, + } +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/typeMap/scalarType.ts b/packages/twenty-client-sdk/src/generate/genql/render/typeMap/scalarType.ts new file mode 100644 index 0000000000..625b02d18d --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/typeMap/scalarType.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { GraphQLEnumType, GraphQLScalarType } from 'graphql' +import { RenderContext } from '../common/RenderContext' +import { Type } from '../../runtime/types' + +export const scalarType = ( + type: GraphQLScalarType | GraphQLEnumType, + _: RenderContext, +): Type => ({}) diff --git a/packages/twenty-client-sdk/src/generate/genql/render/typeMap/support.ts b/packages/twenty-client-sdk/src/generate/genql/render/typeMap/support.ts new file mode 100644 index 0000000000..d71d23eb8d --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/typeMap/support.ts @@ -0,0 +1,7 @@ +// @ts-nocheck +export function isEmpty(x) { + if (!x) { + return true + } + return Object.keys(x).length === 0 +} diff --git a/packages/twenty-client-sdk/src/generate/genql/render/typeMap/unionType.ts b/packages/twenty-client-sdk/src/generate/genql/render/typeMap/unionType.ts new file mode 100644 index 0000000000..2288705c2c --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/render/typeMap/unionType.ts @@ -0,0 +1,22 @@ +// @ts-nocheck +import { GraphQLUnionType } from 'graphql' +import { RenderContext } from '../common/RenderContext' +import { FieldMap } from '../../runtime/types' +import uniq from 'lodash/uniq' + +export const unionType = (type: GraphQLUnionType, _: RenderContext) => { + const types = type.getTypes() + const typeObj: FieldMap = types.reduce>((r, t) => { + r[`on_${t.name}`] = { type: t.name } + return r + }, {}) + + const commonInterfaces = uniq(types.map((x) => x.getInterfaces()).flat()) + commonInterfaces.forEach((t) => { + typeObj[`on_${t.name}`] = { type: t.name } + }) + + typeObj.__typename = { type: 'String' } + + return typeObj +} diff --git a/packages/twenty-client-sdk/src/generate/genql/runtime-templates.ts b/packages/twenty-client-sdk/src/generate/genql/runtime-templates.ts new file mode 100644 index 0000000000..f00ae7eb1c --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/runtime-templates.ts @@ -0,0 +1,25 @@ +// The generated client ships its own copy of the genql runtime. Upstream genql +// reads these files from disk at generation time; we import them as raw text so +// they are bundled into this package and copied verbatim into each generated +// client's `runtime/` folder (with a `// @ts-nocheck` header, as genql does). +import batcher from './runtime/batcher.ts?raw'; +import createClient from './runtime/createClient.ts?raw'; +import error from './runtime/error.ts?raw'; +import fetcher from './runtime/fetcher.ts?raw'; +import generateGraphqlOperation from './runtime/generateGraphqlOperation.ts?raw'; +import index from './runtime/index.ts?raw'; +import linkTypeMap from './runtime/linkTypeMap.ts?raw'; +import typeSelection from './runtime/typeSelection.ts?raw'; +import types from './runtime/types.ts?raw'; + +export const RUNTIME_TEMPLATE_FILES: { name: string; content: string }[] = [ + { name: 'batcher.ts', content: batcher }, + { name: 'createClient.ts', content: createClient }, + { name: 'error.ts', content: error }, + { name: 'fetcher.ts', content: fetcher }, + { name: 'generateGraphqlOperation.ts', content: generateGraphqlOperation }, + { name: 'index.ts', content: index }, + { name: 'linkTypeMap.ts', content: linkTypeMap }, + { name: 'typeSelection.ts', content: typeSelection }, + { name: 'types.ts', content: types }, +]; diff --git a/packages/twenty-client-sdk/src/generate/genql/runtime/batcher.ts b/packages/twenty-client-sdk/src/generate/genql/runtime/batcher.ts new file mode 100644 index 0000000000..877533fd97 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/runtime/batcher.ts @@ -0,0 +1,270 @@ +import type { GraphqlOperation } from './generateGraphqlOperation' +import { GenqlError } from './error' + +type Variables = Record + +type QueryError = Error & { + message: string + + locations?: Array<{ + line: number + column: number + }> + path?: any + rid: string + details?: Record +} +type Result = { + data: Record + errors: Array +} +type Fetcher = ( + batchedQuery: GraphqlOperation | Array, +) => Promise> +type Options = { + batchInterval?: number + shouldBatch?: boolean + maxBatchSize?: number +} +type Queue = Array<{ + request: GraphqlOperation + resolve: (...args: Array) => any + reject: (...args: Array) => any +}> + +/** + * takes a list of requests (queue) and batches them into a single server request. + * It will then resolve each individual requests promise with the appropriate data. + * @private + * @param {QueryBatcher} client - the client to use + * @param {Queue} queue - the list of requests to batch + */ +function dispatchQueueBatch(client: QueryBatcher, queue: Queue): void { + let batchedQuery: any = queue.map((item) => item.request) + + if (batchedQuery.length === 1) { + batchedQuery = batchedQuery[0] + } + + client.fetcher(batchedQuery).then((responses: any) => { + if (queue.length === 1 && !Array.isArray(responses)) { + if (responses.errors && responses.errors.length) { + queue[0].reject( + new GenqlError(responses.errors, responses.data), + ) + return + } + + queue[0].resolve(responses) + return + } else if (responses.length !== queue.length) { + throw new Error('response length did not match query length') + } + + for (let i = 0; i < queue.length; i++) { + if (responses[i].errors && responses[i].errors.length) { + queue[i].reject( + new GenqlError(responses[i].errors, responses[i].data), + ) + } else { + queue[i].resolve(responses[i]) + } + } + }).catch((error: any) => { + // Reject every queued request if the batched fetch fails (e.g. network + // error), otherwise callers hang forever and the rejection is unhandled. + for (const item of queue) { + item.reject(error) + } + }) +} + +/** + * creates a list of requests to batch according to max batch size. + * @private + * @param {QueryBatcher} client - the client to create list of requests from from + * @param {Options} options - the options for the batch + */ +function dispatchQueue(client: QueryBatcher, options: Options): void { + const queue = client._queue + const maxBatchSize = options.maxBatchSize || 0 + client._queue = [] + + if (maxBatchSize > 0 && maxBatchSize < queue.length) { + for (let i = 0; i < queue.length / maxBatchSize; i++) { + dispatchQueueBatch( + client, + queue.slice(i * maxBatchSize, (i + 1) * maxBatchSize), + ) + } + } else { + dispatchQueueBatch(client, queue) + } +} +/** + * Create a batcher client. + * @param {Fetcher} fetcher - A function that can handle the network requests to graphql endpoint + * @param {Options} options - the options to be used by client + * @param {boolean} options.shouldBatch - should the client batch requests. (default true) + * @param {integer} options.batchInterval - duration (in MS) of each batch window. (default 6) + * @param {integer} options.maxBatchSize - max number of requests in a batch. (default 0) + * @param {boolean} options.defaultHeaders - default headers to include with every request + * + * @example + * const fetcher = batchedQuery => fetch('path/to/graphql', { + * method: 'post', + * headers: { + * Accept: 'application/json', + * 'Content-Type': 'application/json', + * }, + * body: JSON.stringify(batchedQuery), + * credentials: 'include', + * }) + * .then(response => response.json()) + * + * const client = new QueryBatcher(fetcher, { maxBatchSize: 10 }) + */ + +export class QueryBatcher { + fetcher: Fetcher + _options: Options + _queue: Queue + + constructor( + fetcher: Fetcher, + { + batchInterval = 6, + shouldBatch = true, + maxBatchSize = 0, + }: Options = {}, + ) { + this.fetcher = fetcher + this._options = { + batchInterval, + shouldBatch, + maxBatchSize, + } + this._queue = [] + } + + /** + * Fetch will send a graphql request and return the parsed json. + * @param {string} query - the graphql query. + * @param {Variables} variables - any variables you wish to inject as key/value pairs. + * @param {[string]} operationName - the graphql operationName. + * @param {Options} overrides - the client options overrides. + * + * @return {promise} resolves to parsed json of server response + * + * @example + * client.fetch(` + * query getHuman($id: ID!) { + * human(id: $id) { + * name + * height + * } + * } + * `, { id: "1001" }, 'getHuman') + * .then(human => { + * // do something with human + * console.log(human); + * }); + */ + fetch( + query: string, + variables?: Variables, + operationName?: string, + overrides: Options = {}, + ): Promise { + const request: GraphqlOperation = { + query, + } + const options = Object.assign({}, this._options, overrides) + + if (variables) { + request.variables = variables + } + + if (operationName) { + request.operationName = operationName + } + + const promise = new Promise((resolve, reject) => { + this._queue.push({ + request, + resolve, + reject, + }) + + if (this._queue.length === 1) { + if (options.shouldBatch) { + setTimeout( + () => dispatchQueue(this, options), + options.batchInterval, + ) + } else { + dispatchQueue(this, options) + } + } + }) + return promise + } + + /** + * Fetch will send a graphql request and return the parsed json. + * @param {string} query - the graphql query. + * @param {Variables} variables - any variables you wish to inject as key/value pairs. + * @param {[string]} operationName - the graphql operationName. + * @param {Options} overrides - the client options overrides. + * + * @return {Promise>} resolves to parsed json of server response + * + * @example + * client.forceFetch(` + * query getHuman($id: ID!) { + * human(id: $id) { + * name + * height + * } + * } + * `, { id: "1001" }, 'getHuman') + * .then(human => { + * // do something with human + * console.log(human); + * }); + */ + forceFetch( + query: string, + variables?: Variables, + operationName?: string, + overrides: Options = {}, + ): Promise { + const request: GraphqlOperation = { + query, + } + const options = Object.assign({}, this._options, overrides, { + shouldBatch: false, + }) + + if (variables) { + request.variables = variables + } + + if (operationName) { + request.operationName = operationName + } + + const promise = new Promise((resolve, reject) => { + const client = new QueryBatcher(this.fetcher, this._options) + client._queue = [ + { + request, + resolve, + reject, + }, + ] + dispatchQueue(client, options) + }) + return promise + } +} diff --git a/packages/twenty-client-sdk/src/generate/genql/runtime/createClient.ts b/packages/twenty-client-sdk/src/generate/genql/runtime/createClient.ts new file mode 100644 index 0000000000..63679b689d --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/runtime/createClient.ts @@ -0,0 +1,67 @@ + +import { type BatchOptions, createFetcher } from './fetcher' +import type { ExecutionResult, LinkedType } from './types' +import { + generateGraphqlOperation, + type GraphqlOperation, +} from './generateGraphqlOperation' + +export type Headers = + | HeadersInit + | (() => HeadersInit) + | (() => Promise) + +export type BaseFetcher = ( + operation: GraphqlOperation | GraphqlOperation[], +) => Promise + +export type ClientOptions = Omit & { + url?: string + batch?: BatchOptions | boolean + fetcher?: BaseFetcher + fetch?: Function + headers?: Headers +} + +export const createClient = ({ + queryRoot, + mutationRoot, + subscriptionRoot, + ...options +}: ClientOptions & { + queryRoot?: LinkedType + mutationRoot?: LinkedType + subscriptionRoot?: LinkedType +}) => { + const fetcher = createFetcher(options) + const client: { + query?: Function + mutation?: Function + } = {} + + if (queryRoot) { + client.query = (request: any) => { + if (!queryRoot) throw new Error('queryRoot argument is missing') + + const resultPromise = fetcher( + generateGraphqlOperation('query', queryRoot, request), + ) + + return resultPromise + } + } + if (mutationRoot) { + client.mutation = (request: any) => { + if (!mutationRoot) + throw new Error('mutationRoot argument is missing') + + const resultPromise = fetcher( + generateGraphqlOperation('mutation', mutationRoot, request), + ) + + return resultPromise + } + } + + return client as any +} diff --git a/packages/twenty-client-sdk/src/generate/genql/runtime/error.ts b/packages/twenty-client-sdk/src/generate/genql/runtime/error.ts new file mode 100644 index 0000000000..7e91e1671d --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/runtime/error.ts @@ -0,0 +1,28 @@ +export class GenqlError extends Error { + errors: Array = [] + /** + * Partial data returned by the server + */ + data?: any + constructor(errors: any[], data: any) { + let message = Array.isArray(errors) + ? errors.map((x) => x?.message || '').join('\n') + : '' + if (!message) { + message = 'GraphQL error' + } + super(message) + this.errors = errors + this.data = data + } +} + +interface GraphqlError { + message: string + locations?: Array<{ + line: number + column: number + }> + path?: string[] + extensions?: Record +} diff --git a/packages/twenty-client-sdk/src/generate/genql/runtime/fetcher.ts b/packages/twenty-client-sdk/src/generate/genql/runtime/fetcher.ts new file mode 100644 index 0000000000..c561bbf005 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/runtime/fetcher.ts @@ -0,0 +1,97 @@ +import { QueryBatcher } from './batcher' + +import type { ClientOptions } from './createClient' +import type { GraphqlOperation } from './generateGraphqlOperation' +import { GenqlError } from './error' + +export interface Fetcher { + (gql: GraphqlOperation): Promise +} + +export type BatchOptions = { + batchInterval?: number // ms + maxBatchSize?: number +} + +const DEFAULT_BATCH_OPTIONS = { + maxBatchSize: 10, + batchInterval: 40, +} + +export const createFetcher = ({ + url, + headers = {}, + fetcher, + fetch: _fetch, + batch = false, + ...rest +}: ClientOptions): Fetcher => { + if (!url && !fetcher) { + throw new Error('url or fetcher is required') + } + if (!fetcher) { + fetcher = async (body) => { + let headersObject = + typeof headers == 'function' ? await headers() : headers + headersObject = headersObject || {} + if (typeof fetch === 'undefined' && !_fetch) { + throw new Error( + 'Global `fetch` function is not available, pass a fetch polyfill to Genql `createClient`', + ) + } + let fetchImpl = _fetch || fetch + const res = await fetchImpl(url!, { + headers: { + 'Content-Type': 'application/json', + ...headersObject, + }, + method: 'POST', + body: JSON.stringify(body), + ...rest, + }) + if (!res.ok) { + throw new Error(`${res.statusText}: ${await res.text()}`) + } + const json = await res.json() + return json + } + } + + if (!batch) { + return async (body) => { + const json = await fetcher!(body) + if (Array.isArray(json)) { + return json.map((json) => { + if (json?.errors?.length) { + throw new GenqlError(json.errors || [], json.data) + } + return json.data + }) + } else { + if (json?.errors?.length) { + throw new GenqlError(json.errors || [], json.data) + } + return json.data + } + } + } + + const batcher = new QueryBatcher( + async (batchedQuery) => { + // console.log(batchedQuery) // [{ query: 'query{user{age}}', variables: {} }, ...] + const json = await fetcher!(batchedQuery) + return json as any + }, + batch === true ? DEFAULT_BATCH_OPTIONS : batch, + ) + + return async ({ query, variables }) => { + const json = await batcher.fetch(query, variables) + if (json?.data) { + return json.data + } + throw new Error( + 'Genql batch fetcher returned unexpected result ' + JSON.stringify(json), + ) + } +} diff --git a/packages/twenty-client-sdk/src/generate/genql/runtime/generateGraphqlOperation.ts b/packages/twenty-client-sdk/src/generate/genql/runtime/generateGraphqlOperation.ts new file mode 100644 index 0000000000..05f9321324 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/runtime/generateGraphqlOperation.ts @@ -0,0 +1,224 @@ +import type { LinkedField, LinkedType } from './types' + +export interface Args { + [arg: string]: any | undefined +} + +export interface Fields { + [field: string]: Request +} + +export type Request = boolean | number | Fields + +export interface Variables { + [name: string]: { + value: any + typing: [LinkedType, string] + } +} + +export interface Context { + root: LinkedType + varCounter: number + variables: Variables + fragmentCounter: number + fragments: string[] +} + +export interface GraphqlOperation { + query: string + variables?: { [name: string]: any } + operationName?: string +} + +const parseRequest = ( + request: Request | undefined, + ctx: Context, + path: string[], +): string => { + if (typeof request === 'object' && '__args' in request) { + const args: any = request.__args + let fields: Request | undefined = { ...request } + delete fields.__args + const argNames = Object.keys(args) + + if (argNames.length === 0) { + return parseRequest(fields, ctx, path) + } + + const field = getFieldFromPath(ctx.root, path) + + const argStrings = argNames.map((argName) => { + ctx.varCounter++ + const varName = `v${ctx.varCounter}` + + const typing = field.args && field.args[argName] // typeMap used here, .args + + if (!typing) { + throw new Error( + `no typing defined for argument \`${argName}\` in path \`${path.join( + '.', + )}\``, + ) + } + + ctx.variables[varName] = { + value: args[argName], + typing, + } + + return `${argName}:$${varName}` + }) + return `(${argStrings})${parseRequest(fields, ctx, path)}` + } else if (typeof request === 'object' && Object.keys(request).length > 0) { + const fields = request + const fieldNames = Object.keys(fields).filter((k) => Boolean(fields[k])) + + if (fieldNames.length === 0) { + throw new Error( + `field selection should not be empty: ${path.join('.')}`, + ) + } + + const type = + path.length > 0 ? getFieldFromPath(ctx.root, path).type : ctx.root + const scalarFields = type.scalar + + let scalarFieldsFragment: string | undefined + + if (fieldNames.includes('__scalar')) { + const falsyFieldNames = new Set( + Object.keys(fields).filter((k) => !Boolean(fields[k])), + ) + if (scalarFields?.length) { + ctx.fragmentCounter++ + scalarFieldsFragment = `f${ctx.fragmentCounter}` + + ctx.fragments.push( + `fragment ${scalarFieldsFragment} on ${ + type.name + }{${scalarFields + .filter((f) => !falsyFieldNames.has(f)) + .join(',')}}`, + ) + } + } + + const fieldsSelection = fieldNames + .filter((f) => !['__scalar', '__name'].includes(f)) + .map((f) => { + const parsed = parseRequest(fields[f], ctx, [...path, f]) + + if (f.startsWith('on_')) { + ctx.fragmentCounter++ + const implementationFragment = `f${ctx.fragmentCounter}` + + const typeMatch = f.match(/^on_(.+)/) + + if (!typeMatch || !typeMatch[1]) + throw new Error('match failed') + + ctx.fragments.push( + `fragment ${implementationFragment} on ${typeMatch[1]}${parsed}`, + ) + + return `...${implementationFragment}` + } else { + return `${f}${parsed}` + } + }) + .concat(scalarFieldsFragment ? [`...${scalarFieldsFragment}`] : []) + .join(',') + + return `{${fieldsSelection}}` + } else { + return '' + } +} + +export const generateGraphqlOperation = ( + operation: 'query' | 'mutation' | 'subscription', + root: LinkedType, + fields?: Fields, +): GraphqlOperation => { + const ctx: Context = { + root: root, + varCounter: 0, + variables: {}, + fragmentCounter: 0, + fragments: [], + } + const result = parseRequest(fields, ctx, []) + + const varNames = Object.keys(ctx.variables) + + const varsString = + varNames.length > 0 + ? `(${varNames.map((v) => { + const variableType = ctx.variables[v].typing[1] + return `$${v}:${variableType}` + })})` + : '' + + const operationName = fields?.__name || '' + + return { + query: [ + `${operation} ${operationName}${varsString}${result}`, + ...ctx.fragments, + ].join(','), + variables: Object.keys(ctx.variables).reduce<{ [name: string]: any }>( + (r, v) => { + r[v] = ctx.variables[v].value + return r + }, + {}, + ), + ...(operationName ? { operationName: operationName.toString() } : {}), + } +} + +export const getFieldFromPath = ( + root: LinkedType | undefined, + path: string[], +) => { + let current: LinkedField | undefined + + if (!root) throw new Error('root type is not provided') + + if (path.length === 0) throw new Error(`path is empty`) + + path.forEach((f) => { + const type = current ? current.type : root + + if (!type.fields) + throw new Error(`type \`${type.name}\` does not have fields`) + + const possibleTypes = Object.keys(type.fields) + .filter((i) => i.startsWith('on_')) + .reduce( + (types, fieldName) => { + const field = type.fields && type.fields[fieldName] + if (field) types.push(field.type) + return types + }, + [type], + ) + + let field: LinkedField | null = null + + possibleTypes.forEach((type) => { + const found = type.fields && type.fields[f] + if (found) field = found + }) + + if (!field) + throw new Error( + `type \`${type.name}\` does not have a field \`${f}\``, + ) + + current = field + }) + + return current as LinkedField +} diff --git a/packages/twenty-client-sdk/src/generate/genql/runtime/index.ts b/packages/twenty-client-sdk/src/generate/genql/runtime/index.ts new file mode 100644 index 0000000000..68d601e1f6 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/runtime/index.ts @@ -0,0 +1,12 @@ +export { createClient } from './createClient' +export type { ClientOptions } from './createClient' +export type { FieldsSelection } from './typeSelection' +export { generateGraphqlOperation } from './generateGraphqlOperation' +export type { GraphqlOperation } from './generateGraphqlOperation' +export { linkTypeMap } from './linkTypeMap' +// export { Observable } from 'zen-observable-ts' +export { createFetcher } from './fetcher' +export { GenqlError } from './error' +export const everything = { + __scalar: true, +} diff --git a/packages/twenty-client-sdk/src/generate/genql/runtime/linkTypeMap.ts b/packages/twenty-client-sdk/src/generate/genql/runtime/linkTypeMap.ts new file mode 100644 index 0000000000..c5e5f56744 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/runtime/linkTypeMap.ts @@ -0,0 +1,138 @@ +import type { + CompressedType, + CompressedTypeMap, + LinkedArgMap, + LinkedField, + LinkedType, + LinkedTypeMap, +} from './types' + +export interface PartialLinkedFieldMap { + [field: string]: { + type: string + args?: LinkedArgMap + } +} + +export const linkTypeMap = ( + typeMap: CompressedTypeMap, +): LinkedTypeMap => { + const indexToName: Record = Object.assign( + {}, + ...Object.keys(typeMap.types).map((k, i) => ({ [i]: k })), + ) + + let intermediaryTypeMap = Object.assign( + {}, + ...Object.keys(typeMap.types || {}).map( + (k): Record => { + const type: CompressedType = typeMap.types[k]! + const fields = type || {} + return { + [k]: { + name: k, + // type scalar properties + scalar: Object.keys(fields).filter((f) => { + const [type] = fields[f] || [] + return type && typeMap.scalars.includes(type) + }), + // fields with corresponding `type` and `args` + fields: Object.assign( + {}, + ...Object.keys(fields).map( + (f): PartialLinkedFieldMap => { + const [typeIndex, args] = fields[f] || [] + if (typeIndex == null) { + return {} + } + return { + [f]: { + // replace index with type name + type: indexToName[typeIndex], + args: Object.assign( + {}, + ...Object.keys(args || {}).map( + (k) => { + // if argTypeString == argTypeName, argTypeString is missing, need to readd it + if (!args || !args[k]) { + return + } + const [ + argTypeName, + argTypeString, + ] = args[k] as any + return { + [k]: [ + indexToName[ + argTypeName + ], + argTypeString || + indexToName[ + argTypeName + ], + ], + } + }, + ), + ), + }, + } + }, + ), + ), + }, + } + }, + ), + ) + const res = resolveConcreteTypes(intermediaryTypeMap) + return res +} + +// replace typename with concrete type +export const resolveConcreteTypes = (linkedTypeMap: LinkedTypeMap) => { + Object.keys(linkedTypeMap).forEach((typeNameFromKey) => { + const type: LinkedType = linkedTypeMap[typeNameFromKey]! + // type.name = typeNameFromKey + if (!type.fields) { + return + } + + const fields = type.fields + + Object.keys(fields).forEach((f) => { + const field: LinkedField = fields[f]! + + if (field.args) { + const args = field.args + Object.keys(args).forEach((key) => { + const arg = args[key] + + if (arg) { + const [typeName] = arg + + if (typeof typeName === 'string') { + if (!linkedTypeMap[typeName]) { + linkedTypeMap[typeName] = { name: typeName } + } + + arg[0] = linkedTypeMap[typeName]! + } + } + }) + } + + const typeName = field.type as LinkedType | string + + if (typeof typeName === 'string') { + if (!linkedTypeMap[typeName]) { + linkedTypeMap[typeName] = { name: typeName } + } + + field.type = linkedTypeMap[typeName]! + } + }) + }) + + return linkedTypeMap +} diff --git a/packages/twenty-client-sdk/src/generate/genql/runtime/typeSelection.ts b/packages/twenty-client-sdk/src/generate/genql/runtime/typeSelection.ts new file mode 100644 index 0000000000..9a8de124d6 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/runtime/typeSelection.ts @@ -0,0 +1,97 @@ +////////////////////////////////////////////////// + +// SOME THINGS TO KNOW BEFORE DIVING IN +/* +0. DST is the request type, SRC is the response type + +1. FieldsSelection uses an object because currently is impossible to make recursive types + +2. FieldsSelection is a recursive type that makes a type based on request type and fields + +3. HandleObject handles object types + +4. Handle__scalar adds all scalar properties excluding non scalar props +*/ + +export type FieldsSelection | undefined, DST> = { + scalar: SRC + union: Handle__isUnion + object: HandleObject + array: SRC extends Nil + ? never + : SRC extends (infer T)[] + ? Array> + : never + __scalar: Handle__scalar + never: never +}[DST extends Nil + ? 'never' + : SRC extends Nil + ? 'never' + : DST extends false | 0 + ? 'never' + : SRC extends Scalar + ? 'scalar' + : SRC extends any[] + ? 'array' + : SRC extends { __isUnion?: any } + ? 'union' + : DST extends { __scalar?: any } + ? '__scalar' + : DST extends {} + ? 'object' + : 'never'] + +type HandleObject, DST> = SRC extends Nil + ? never + : Pick< + { + // using keyof SRC to maintain ?: relations of SRC type + [Key in keyof SRC]: Key extends keyof DST + ? FieldsSelection< + NonNullable, + NonNullable + > + : SRC[Key] + }, + Exclude + // { + // // remove falsy values + // [Key in keyof DST]: DST[Key] extends false | 0 ? never : Key + // }[keyof DST] + > + +type Handle__scalar, DST> = SRC extends Nil + ? never + : Pick< + // continue processing fields that are in DST, directly pass SRC type if not in DST + { + [Key in keyof SRC]: Key extends keyof DST + ? FieldsSelection + : SRC[Key] + }, + // remove fields that are not scalars or are not in DST + { + [Key in keyof SRC]: SRC[Key] extends Nil + ? never + : Key extends FieldsToRemove + ? never + : SRC[Key] extends Scalar + ? Key + : Key extends keyof DST + ? Key + : never + }[keyof SRC] + > + +type Handle__isUnion, DST> = SRC extends Nil + ? never + : Omit // just return the union type + +type Scalar = string | number | Date | boolean | null | undefined + +type Anify = { [P in keyof T]?: any } + +type FieldsToRemove = '__isUnion' | '__scalar' | '__name' | '__args' + +type Nil = undefined | null diff --git a/packages/twenty-client-sdk/src/generate/genql/runtime/types.ts b/packages/twenty-client-sdk/src/generate/genql/runtime/types.ts new file mode 100644 index 0000000000..35376a7d5a --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/runtime/types.ts @@ -0,0 +1,68 @@ + +export interface ExecutionResult { + errors?: Array + data?: TData | null +} + +export interface ArgMap { + [arg: string]: [keyType, string] | [keyType] | undefined +} + +export type CompressedField = [ + type: keyType, + args?: ArgMap, +] + +export interface CompressedFieldMap { + [field: string]: CompressedField | undefined +} + +export type CompressedType = CompressedFieldMap + +export interface CompressedTypeMap { + scalars: Array + types: { + [type: string]: CompressedType | undefined + } +} + +// normal types +export type Field = { + type: keyType + args?: ArgMap +} + +export interface FieldMap { + [field: string]: Field | undefined +} + +export type Type = FieldMap + +export interface TypeMap { + scalars: Array + types: { + [type: string]: Type | undefined + } +} + +export interface LinkedArgMap { + [arg: string]: [LinkedType, string] | undefined +} +export interface LinkedField { + type: LinkedType + args?: LinkedArgMap +} + +export interface LinkedFieldMap { + [field: string]: LinkedField | undefined +} + +export interface LinkedType { + name: string + fields?: LinkedFieldMap + scalar?: string[] +} + +export interface LinkedTypeMap { + [type: string]: LinkedType | undefined +} diff --git a/packages/twenty-client-sdk/src/generate/genql/tasks/clientTasks.ts b/packages/twenty-client-sdk/src/generate/genql/tasks/clientTasks.ts new file mode 100644 index 0000000000..1dff66ede0 --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/tasks/clientTasks.ts @@ -0,0 +1,103 @@ +import { GraphQLEnumType, GraphQLSchema, isEnumType } from 'graphql'; +import camelCase from 'lodash/camelCase'; + +import { type Config } from '../config'; +import { ensurePath, writeFileToPath } from '../helpers/files'; +import { renderClientEsm } from '../render/client/renderClient'; +import { excludedTypes } from '../render/common/excludedTypes'; +import { RenderContext } from '../render/common/RenderContext'; +import { renderRequestTypes } from '../render/requestTypes/renderRequestTypes'; +import { renderResponseTypes } from '../render/responseTypes/renderResponseTypes'; +import { renderSchema } from '../render/schema/renderSchema'; +import { renderTypeGuards } from '../render/typeGuards/renderTypeGuards'; +import { renderTypeMap } from '../render/typeMap/renderTypeMap'; +import { RUNTIME_TEMPLATE_FILES } from '../runtime-templates'; + +const schemaTypesFile = 'schema.ts'; +const schemaGqlFile = 'schema.graphql'; +const typeMapFileEsm = 'types.ts'; +const clientFileEsm = 'index.ts'; + +// Writes the generated client files to `config.output`. Upstream genql ran each +// write as a concurrent listr task; we run them sequentially (file contents are +// identical, and dropping listr removes a dependency). +export const writeClientFiles = async ( + config: Config, + schema: GraphQLSchema, +): Promise => { + if (!config.output) { + throw new Error('`output` must be defined in the config'); + } + + const output = config.output; + + await ensurePath([output], true); + + const schemaGqlCtx = new RenderContext(schema, config); + renderSchema(schema, schemaGqlCtx); + await writeFileToPath([output, schemaGqlFile], schemaGqlCtx.toCode('graphql')); + + await ensurePath([output, 'runtime']); + for (const { name, content } of RUNTIME_TEMPLATE_FILES) { + await writeFileToPath([output, 'runtime', name], '// @ts-nocheck\n' + content); + } + + const schemaTypesCtx = new RenderContext(schema, config); + renderResponseTypes(schema, schemaTypesCtx); + renderRequestTypes(schema, schemaTypesCtx); + renderTypeGuards(schema, schemaTypesCtx); + renderEnumsMaps(schema, schemaTypesCtx); + await writeFileToPath( + [output, schemaTypesFile], + '// @ts-nocheck\n' + schemaTypesCtx.toCode('typescript'), + ); + + const typeMapCtx = new RenderContext(schema, config); + renderTypeMap(schema, typeMapCtx); + await writeFileToPath( + [output, typeMapFileEsm], + `export default ${typeMapCtx.toCode()}`, + ); + + const clientCtx = new RenderContext(schema, config); + renderClientEsm(schema, clientCtx); + await writeFileToPath( + [output, clientFileEsm], + '// @ts-nocheck\n' + clientCtx.toCode('typescript', true), + ); +}; + +function renderEnumsMaps(schema: GraphQLSchema, ctx: RenderContext) { + let typeMap = schema.getTypeMap(); + + const enums: GraphQLEnumType[] = []; + for (const name in typeMap) { + if (excludedTypes.includes(name)) continue; + + const type = typeMap[name]; + + if (isEnumType(type)) { + enums.push(type); + } + } + if (enums.length === 0) return; + + ctx.addCodeBlock( + enums + .map( + (type) => + `export const ${camelCase('enum' + type.name)} = {\n` + + type + .getValues() + .map((v) => { + if (!v?.name) { + return ''; + } + return ` ${v.name}: '${v.name}' as const`; + }) + .join(',\n') + + `\n}\n`, + ) + .join('\n'), + ); +} diff --git a/packages/twenty-client-sdk/src/generate/genql/tasks/schemaTask.ts b/packages/twenty-client-sdk/src/generate/genql/tasks/schemaTask.ts new file mode 100644 index 0000000000..260c46708e --- /dev/null +++ b/packages/twenty-client-sdk/src/generate/genql/tasks/schemaTask.ts @@ -0,0 +1,36 @@ +import { + assertValidSchema, + buildSchema, + GraphQLSchema, + lexicographicSortSchema, +} from 'graphql'; + +import { type Config } from '../config'; + +// Builds the schema from the config SDL string. The upstream genql codegen used +// @graphql-tools' loadSchema and also supported fetching the schema from a live +// endpoint (via undici/native-fetch). Twenty always passes an SDL string, so we +// build it directly with graphql's buildSchema — no @graphql-tools or network. +export const loadConfiguredSchema = async ( + config: Config, +): Promise => { + if (!config.schema) { + throw new Error('`schema` must be defined in the config'); + } + + const document = buildSchema(config.schema, { assumeValidSDL: true }); + + const schema = config.sortProperties + ? lexicographicSortSchema(document) + : document; + + // A schema without a Query root is still renderable (e.g. metadata-only), + // matching upstream genql behaviour, so only run full validation — which + // requires a Query root — when one is present. (Checking the root directly + // avoids depending on a specific graphql error-message string.) + if (schema.getQueryType()) { + assertValidSchema(schema); + } + + return schema; +}; diff --git a/packages/twenty-client-sdk/src/metadata/generated/runtime/batcher.ts b/packages/twenty-client-sdk/src/metadata/generated/runtime/batcher.ts index 53b775f4d7..eedcda3fbc 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/runtime/batcher.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/runtime/batcher.ts @@ -71,6 +71,12 @@ function dispatchQueueBatch(client: QueryBatcher, queue: Queue): void { queue[i].resolve(responses[i]) } } + }).catch((error: any) => { + // Reject every queued request if the batched fetch fails (e.g. network + // error), otherwise callers hang forever and the rejection is unhandled. + for (const item of queue) { + item.reject(error) + } }) } diff --git a/packages/twenty-client-sdk/vite.config.ts b/packages/twenty-client-sdk/vite.config.ts index b2dcc4848b..b66146d2ad 100644 --- a/packages/twenty-client-sdk/vite.config.ts +++ b/packages/twenty-client-sdk/vite.config.ts @@ -58,6 +58,7 @@ export default defineConfig(() => { 'node:fs/promises', 'node:fs', 'node:path', + 'node:os', ], output: [ { diff --git a/packages/twenty-sdk/package.json b/packages/twenty-sdk/package.json index 5135cabfce..ed57aeb470 100644 --- a/packages/twenty-sdk/package.json +++ b/packages/twenty-sdk/package.json @@ -91,7 +91,6 @@ "uuid": "^13.0.0" }, "devDependencies": { - "@genql/cli": "^3.0.3", "@prettier/sync": "^0.5.2", "@types/node": "^24.0.0", "@types/react": "^19.0.0", diff --git a/yarn.lock b/yarn.lock index b0b2bdc4d0..b20983eab5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7453,13 +7453,6 @@ __metadata: languageName: node linkType: hard -"@fastify/busboy@npm:^2.0.0": - version: 2.1.1 - resolution: "@fastify/busboy@npm:2.1.1" - checksum: 10c0/6f8027a8cba7f8f7b736718b013f5a38c0476eea67034c94a0d3c375e2b114366ad4419e6a6fa7ffc2ef9c6d3e0435d76dd584a7a1cbac23962fda7650b579e3 - languageName: node - linkType: hard - "@fastify/otel@npm:0.18.0": version: 0.18.0 resolution: "@fastify/otel@npm:0.18.0" @@ -7684,27 +7677,23 @@ __metadata: languageName: node linkType: hard -"@genql/cli@npm:^3.0.3": - version: 3.0.5 - resolution: "@genql/cli@npm:3.0.5" +"@genql/runtime@npm:^2.10.0": + version: 2.10.0 + resolution: "@genql/runtime@npm:2.10.0" dependencies: - "@graphql-tools/graphql-file-loader": "npm:^7.5.11" - "@graphql-tools/load": "npm:^7.8.6" - fs-extra: "npm:^10.1.0" - graphql: "npm:^16.6.0" - kleur: "npm:^4.1.5" - listr: "npm:^0.14.3" - lodash: "npm:^4.17.21" - mkdirp: "npm:^0.5.1" - native-fetch: "npm:^4.0.2" - prettier: "npm:^2.8.0" - qs: "npm:^6.11.0" - rimraf: "npm:^2.6.3" - undici: "npm:^5.18.0" - yargs: "npm:^15.3.1" - bin: - genql: dist/cli.js - checksum: 10c0/646d4da8986741a8f1b610bd8cfb5bc26cd9a856de671461e4ca87fb05443f37dd0edfb3b30ceaa34cd049077873afa07febfd4714dd2e0681fb07484e19a080 + "@types/qs": "npm:^6.9.0" + "@types/ws": "npm:^6.0.1" + graphql-query-batcher: "npm:^1.0.1" + isomorphic-unfetch: "npm:^3.0.0" + lodash: "npm:^4.17.20" + subscriptions-transport-ws: "npm:^0.9.16" + tslib: "npm:^2.0.0" + utility-types: "npm:^3.10.0" + ws: "npm:^6.1.4" + zen-observable-ts: "npm:^0.8.21" + peerDependencies: + graphql: "*" + checksum: 10c0/e2a886c2469c933681e2b0ddd6a5b7f4cb12932251ba460e3cf2db4246817da79313ea4ba9769ec7cbe53ab9c1cb81ad8fcce6a969cd241185b79398d2a4f3c6 languageName: node linkType: hard @@ -8175,7 +8164,7 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/graphql-file-loader@npm:^7.3.7, @graphql-tools/graphql-file-loader@npm:^7.5.0, @graphql-tools/graphql-file-loader@npm:^7.5.11": +"@graphql-tools/graphql-file-loader@npm:^7.3.7, @graphql-tools/graphql-file-loader@npm:^7.5.0": version: 7.5.17 resolution: "@graphql-tools/graphql-file-loader@npm:7.5.17" dependencies: @@ -8233,7 +8222,7 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/load@npm:^7.5.5, @graphql-tools/load@npm:^7.8.0, @graphql-tools/load@npm:^7.8.6": +"@graphql-tools/load@npm:^7.5.5, @graphql-tools/load@npm:^7.8.0": version: 7.8.14 resolution: "@graphql-tools/load@npm:7.8.14" dependencies: @@ -18937,20 +18926,6 @@ __metadata: languageName: node linkType: hard -"@samverschueren/stream-to-observable@npm:^0.3.0": - version: 0.3.1 - resolution: "@samverschueren/stream-to-observable@npm:0.3.1" - dependencies: - any-observable: "npm:^0.3.0" - peerDependenciesMeta: - rxjs: - optional: true - zen-observable: - optional: true - checksum: 10c0/0d874453f6bc2460d71783292291f52feb36c2a75314b1072a6ffe6206562f33e9d664a554348d565a6b54da9041d75070371052545bc329caaa52f64216987f - languageName: node - linkType: hard - "@scalar/api-client@npm:2.2.62": version: 2.2.62 resolution: "@scalar/api-client@npm:2.2.62" @@ -23435,6 +23410,13 @@ __metadata: languageName: node linkType: hard +"@types/lodash@npm:^4.17.15": + version: 4.17.24 + resolution: "@types/lodash@npm:4.17.24" + checksum: 10c0/b72f60d4daacdad1fa643edb3faba204c02a01eb1ac00a83ff73496a6d236fc55e459c06106e8ced42277dba932d087d8fc090f8de4ef590d3f91e6d6f7ce85a + languageName: node + linkType: hard + "@types/long@npm:^4.0.0": version: 4.0.2 resolution: "@types/long@npm:4.0.2" @@ -24304,6 +24286,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:^6.0.1": + version: 6.0.4 + resolution: "@types/ws@npm:6.0.4" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/fa958e64596ca9487c3ed6012834de70b47f25d971f1950cfb8e6a99cb77ff340ae82ac7627744e01b58010674ef8ede07d5a2ac29ca9ad0d67a430fcc69ae14 + languageName: node + linkType: hard + "@types/ws@npm:^8.0.0": version: 8.5.12 resolution: "@types/ws@npm:8.5.12" @@ -27600,7 +27591,7 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^3.0.0, ansi-escapes@npm:^3.1.0": +"ansi-escapes@npm:^3.1.0": version: 3.2.0 resolution: "ansi-escapes@npm:3.2.0" checksum: 10c0/084e1ce38139ad2406f18a8e7efe2b850ddd06ce3c00f633392d1ce67756dab44fe290e573d09ef3c9a0cb13c12881e0e35a8f77a017d39a0a4ab85ae2fae04f @@ -27666,13 +27657,6 @@ __metadata: languageName: node linkType: hard -"ansi-regex@npm:^3.0.0": - version: 3.0.1 - resolution: "ansi-regex@npm:3.0.1" - checksum: 10c0/d108a7498b8568caf4a46eea4f1784ab4e0dfb2e3f3938c697dee21443d622d765c958f2b7e2b9f6b9e55e2e2af0584eaa9915d51782b89a841c28e744e7a167 - languageName: node - linkType: hard - "ansi-regex@npm:^4.1.0": version: 4.1.1 resolution: "ansi-regex@npm:4.1.1" @@ -27701,13 +27685,6 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^2.2.1": - version: 2.2.1 - resolution: "ansi-styles@npm:2.2.1" - checksum: 10c0/7c68aed4f1857389e7a12f85537ea5b40d832656babbf511cc7ecd9efc52889b9c3e5653a71a6aade783c3c5e0aa223ad4ff8e83c27ac8a666514e6c79068cab - languageName: node - linkType: hard - "ansi-styles@npm:^3.2.1": version: 3.2.1 resolution: "ansi-styles@npm:3.2.1" @@ -27768,13 +27745,6 @@ __metadata: languageName: node linkType: hard -"any-observable@npm:^0.3.0": - version: 0.3.0 - resolution: "any-observable@npm:0.3.0" - checksum: 10c0/104c2b79c2ac7e6c75b35f8fd62babf73015668f22bd25336c6f848350d91f9e7daf2fddbf1c1b76fe795e89fbc91b49f70a2aec5c69f1acf0562c344f36042b - languageName: node - linkType: hard - "any-promise@npm:^1.0.0": version: 1.3.0 resolution: "any-promise@npm:1.3.0" @@ -28352,6 +28322,13 @@ __metadata: languageName: node linkType: hard +"async-limiter@npm:~1.0.0": + version: 1.0.1 + resolution: "async-limiter@npm:1.0.1" + checksum: 10c0/0693d378cfe86842a70d4c849595a0bb50dc44c11649640ca982fa90cbfc74e3cc4753b5a0847e51933f2e9c65ce8e05576e75e5e1fd963a086e673735b35969 + languageName: node + linkType: hard + "async-retry@npm:1.2.3": version: 1.2.3 resolution: "async-retry@npm:1.2.3" @@ -30186,19 +30163,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^1.0.0, chalk@npm:^1.1.3": - version: 1.1.3 - resolution: "chalk@npm:1.1.3" - dependencies: - ansi-styles: "npm:^2.2.1" - escape-string-regexp: "npm:^1.0.2" - has-ansi: "npm:^2.0.0" - strip-ansi: "npm:^3.0.0" - supports-color: "npm:^2.0.0" - checksum: 10c0/28c3e399ec286bb3a7111fd4225ebedb0d7b813aef38a37bca7c498d032459c265ef43404201d5fbb8d888d29090899c95335b4c0cda13e8b126ff15c541cef8 - languageName: node - linkType: hard - "chalk@npm:^2.3.0, chalk@npm:^2.4.1": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -30561,15 +30525,6 @@ __metadata: languageName: node linkType: hard -"cli-cursor@npm:^2.0.0, cli-cursor@npm:^2.1.0": - version: 2.1.0 - resolution: "cli-cursor@npm:2.1.0" - dependencies: - restore-cursor: "npm:^2.0.0" - checksum: 10c0/09ee6d8b5b818d840bf80ec9561eaf696672197d3a02a7daee2def96d5f52ce6e0bbe7afca754ccf14f04830b5a1b4556273e983507d5029f95bba3016618eda - languageName: node - linkType: hard - "cli-cursor@npm:^4.0.0": version: 4.0.0 resolution: "cli-cursor@npm:4.0.0" @@ -30662,16 +30617,6 @@ __metadata: languageName: node linkType: hard -"cli-truncate@npm:^0.2.1": - version: 0.2.1 - resolution: "cli-truncate@npm:0.2.1" - dependencies: - slice-ansi: "npm:0.0.4" - string-width: "npm:^1.0.1" - checksum: 10c0/c6caa5e2b70d841c42f4a2270d6fc7129df915f8911e4afa90c79231ccc857cd819a2c90e0707fde04e51ce56b4d71646b843f6cbaff4f7cdcb3b91ed51f6e89 - languageName: node - linkType: hard - "cli-truncate@npm:^2.1.0": version: 2.1.0 resolution: "cli-truncate@npm:2.1.0" @@ -31928,7 +31873,6 @@ __metadata: version: 0.0.0-use.local resolution: "create-twenty-app@workspace:packages/create-twenty-app" dependencies: - "@genql/cli": "npm:^3.0.3" "@swc/core": "npm:^1.15.11" "@swc/jest": "npm:^0.2.39" "@types/fs-extra": "npm:^11.0.0" @@ -32671,13 +32615,6 @@ __metadata: languageName: node linkType: hard -"date-fns@npm:^1.27.2": - version: 1.30.1 - resolution: "date-fns@npm:1.30.1" - checksum: 10c0/bad6ad7c15180121e15d61ad62a4a214c108d66f35b35f5eeb6ade837a3c29aa4444b9528a93a5374b95ba11231c142276351bf52f4d168676f9a1e17ce3726a - languageName: node - linkType: hard - "date-fns@npm:^3.3.1, date-fns@npm:^3.6.0": version: 3.6.0 resolution: "date-fns@npm:3.6.0" @@ -33914,13 +33851,6 @@ __metadata: languageName: node linkType: hard -"elegant-spinner@npm:^1.0.1": - version: 1.0.1 - resolution: "elegant-spinner@npm:1.0.1" - checksum: 10c0/df607c83c20fc3ce56c514175dd5d1ee7f667da00cee13d04d32c70d55e76555091fa236689e691cf7dedba17b0020fec635e499cdde84dbea2ef8639314e5f8 - languageName: node - linkType: hard - "elliptic@npm:^6.5.3, elliptic@npm:^6.5.5": version: 6.6.1 resolution: "elliptic@npm:6.6.1" @@ -35993,25 +35923,6 @@ __metadata: languageName: node linkType: hard -"figures@npm:^1.7.0": - version: 1.7.0 - resolution: "figures@npm:1.7.0" - dependencies: - escape-string-regexp: "npm:^1.0.5" - object-assign: "npm:^4.1.0" - checksum: 10c0/a10942b0eec3372bf61822ab130d2bbecdf527d551b0b013fbe7175b7a0238ead644ee8930a1a3cb872fb9ab2ec27df30e303765a3b70b97852e2e9ee43bdff3 - languageName: node - linkType: hard - -"figures@npm:^2.0.0": - version: 2.0.0 - resolution: "figures@npm:2.0.0" - dependencies: - escape-string-regexp: "npm:^1.0.5" - checksum: 10c0/5dc5a75fec3e7e04ae65d6ce51d28b3e70d4656c51b06996b6fdb2cb5b542df512e3b3c04482f5193a964edddafa5521479ff948fa84e12ff556e53e094ab4ce - languageName: node - linkType: hard - "file-saver@npm:^2.0.5": version: 2.0.5 resolution: "file-saver@npm:2.0.5" @@ -37591,6 +37502,13 @@ __metadata: languageName: node linkType: hard +"graphql-query-batcher@npm:^1.0.1": + version: 1.0.1 + resolution: "graphql-query-batcher@npm:1.0.1" + checksum: 10c0/804d0f4064721a2116a16b9eac422e9233e85f4ab5b250cb8f83662725658ffde35779a8ae8211037f3dd2f9717de8cb63b394ad8057ce22171a83db2471196a + languageName: node + linkType: hard + "graphql-redis-subscriptions@npm:2.7.0": version: 2.7.0 resolution: "graphql-redis-subscriptions@npm:2.7.0" @@ -37895,15 +37813,6 @@ __metadata: languageName: node linkType: hard -"has-ansi@npm:^2.0.0": - version: 2.0.0 - resolution: "has-ansi@npm:2.0.0" - dependencies: - ansi-regex: "npm:^2.0.0" - checksum: 10c0/f54e4887b9f8f3c4bfefd649c48825b3c093987c92c27880ee9898539e6f01aed261e82e73153c3f920fde0db5bf6ebd58deb498ed1debabcb4bc40113ccdf05 - languageName: node - linkType: hard - "has-bigints@npm:^1.0.2": version: 1.0.2 resolution: "has-bigints@npm:1.0.2" @@ -39248,7 +39157,7 @@ __metadata: languageName: node linkType: hard -"indent-string@npm:^3.0.0, indent-string@npm:^3.2.0": +"indent-string@npm:^3.2.0": version: 3.2.0 resolution: "indent-string@npm:3.2.0" checksum: 10c0/91b6d61621d24944c5c4d365d6f1ff4a490264ccaf1162a602faa0d323e69231db2180ad4ccc092c2f49cf8888cdb3da7b73e904cc0fdeec40d0bfb41ceb9478 @@ -39922,13 +39831,6 @@ __metadata: languageName: node linkType: hard -"is-fullwidth-code-point@npm:^2.0.0": - version: 2.0.0 - resolution: "is-fullwidth-code-point@npm:2.0.0" - checksum: 10c0/e58f3e4a601fc0500d8b2677e26e9fe0cd450980e66adb29d85b6addf7969731e38f8e43ed2ec868a09c101a55ac3d8b78902209269f38c5286bc98f5bc1b4d9 - languageName: node - linkType: hard - "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" @@ -40169,15 +40071,6 @@ __metadata: languageName: node linkType: hard -"is-observable@npm:^1.1.0": - version: 1.1.0 - resolution: "is-observable@npm:1.1.0" - dependencies: - symbol-observable: "npm:^1.1.0" - checksum: 10c0/cf3166b0822f70ad06e7851e09430166ce658349d54aaa64c93a03320420b9285735821b23164bdce741ff83a86730ac3e53035ce4e2511ed843dbff4105bfa2 - languageName: node - linkType: hard - "is-online@npm:^10.0.0": version: 10.0.0 resolution: "is-online@npm:10.0.0" @@ -40563,6 +40456,16 @@ __metadata: languageName: node linkType: hard +"isomorphic-unfetch@npm:^3.0.0": + version: 3.1.0 + resolution: "isomorphic-unfetch@npm:3.1.0" + dependencies: + node-fetch: "npm:^2.6.1" + unfetch: "npm:^4.2.0" + checksum: 10c0/d3b61fca06304db692b7f76bdfd3a00f410e42cfa7403c3b250546bf71589d18cf2f355922f57198e4cc4a9872d3647b20397a5c3edf1a347c90d57c83cf2a89 + languageName: node + linkType: hard + "isomorphic-ws@npm:5.0.0, isomorphic-ws@npm:^5.0.0": version: 5.0.0 resolution: "isomorphic-ws@npm:5.0.0" @@ -42745,43 +42648,6 @@ __metadata: languageName: node linkType: hard -"listr-silent-renderer@npm:^1.1.1": - version: 1.1.1 - resolution: "listr-silent-renderer@npm:1.1.1" - checksum: 10c0/a13e08ebf863516a757bce4887f05290070772113d89095e9f51a07cf0b11a43a7563a67ff3b287c752c08f6d781fdb2123b02957534e3e0675fb564f2a42e1b - languageName: node - linkType: hard - -"listr-update-renderer@npm:^0.5.0": - version: 0.5.0 - resolution: "listr-update-renderer@npm:0.5.0" - dependencies: - chalk: "npm:^1.1.3" - cli-truncate: "npm:^0.2.1" - elegant-spinner: "npm:^1.0.1" - figures: "npm:^1.7.0" - indent-string: "npm:^3.0.0" - log-symbols: "npm:^1.0.2" - log-update: "npm:^2.3.0" - strip-ansi: "npm:^3.0.1" - peerDependencies: - listr: ^0.14.2 - checksum: 10c0/8ade44bf3dc6146c8e0178000619439e8889792c4689b66be6ce82bd459f5fe462ecb34b05147fb206a8ad60e6d4e6f34c9f48038e18366f867fd972688b8edc - languageName: node - linkType: hard - -"listr-verbose-renderer@npm:^0.5.0": - version: 0.5.0 - resolution: "listr-verbose-renderer@npm:0.5.0" - dependencies: - chalk: "npm:^2.4.1" - cli-cursor: "npm:^2.1.0" - date-fns: "npm:^1.27.2" - figures: "npm:^2.0.0" - checksum: 10c0/041cd1e82da7054f27ae0a914e98b40d15faf9f950ef850578fc6241d3fff3c2d7158a4f6226006e566b4c47bf445be2d254dd1ce5c16569a3a5dcd575bec656 - languageName: node - linkType: hard - "listr2@npm:^4.0.5": version: 4.0.5 resolution: "listr2@npm:4.0.5" @@ -42817,23 +42683,6 @@ __metadata: languageName: node linkType: hard -"listr@npm:^0.14.3": - version: 0.14.3 - resolution: "listr@npm:0.14.3" - dependencies: - "@samverschueren/stream-to-observable": "npm:^0.3.0" - is-observable: "npm:^1.1.0" - is-promise: "npm:^2.1.0" - is-stream: "npm:^1.1.0" - listr-silent-renderer: "npm:^1.1.1" - listr-update-renderer: "npm:^0.5.0" - listr-verbose-renderer: "npm:^0.5.0" - p-map: "npm:^2.0.0" - rxjs: "npm:^6.3.3" - checksum: 10c0/753d518218c423f46bee8eeacccecadfd2e414ba9c0f602e7f85fe3f6fa18404dfab0812433aeda4683ee2548358488f597ac1a3d321196baec5d3149b200b10 - languageName: node - linkType: hard - "load-esm@npm:1.0.3": version: 1.0.3 resolution: "load-esm@npm:1.0.3" @@ -43260,15 +43109,6 @@ __metadata: languageName: node linkType: hard -"log-symbols@npm:^1.0.2": - version: 1.0.2 - resolution: "log-symbols@npm:1.0.2" - dependencies: - chalk: "npm:^1.0.0" - checksum: 10c0/c64e1fe41d0d043840f8b592d043b8607a836b846506f525a53d99d578561f02f97b2cba1d2b3c30bae5311d64b308d5a392a9930d252b906a9042fc2877da7a - languageName: node - linkType: hard - "log-symbols@npm:^4.0.0, log-symbols@npm:^4.1.0": version: 4.1.0 resolution: "log-symbols@npm:4.1.0" @@ -43299,17 +43139,6 @@ __metadata: languageName: node linkType: hard -"log-update@npm:^2.3.0": - version: 2.3.0 - resolution: "log-update@npm:2.3.0" - dependencies: - ansi-escapes: "npm:^3.0.0" - cli-cursor: "npm:^2.0.0" - wrap-ansi: "npm:^3.0.1" - checksum: 10c0/9bf21b138801ab4770a2bfa735161cf005b869360eaf5003a84ba64ddc5f5c3ce7217f4f1fa79d9c1f510d792213b2c9800327228e94df05859d19b716215d90 - languageName: node - linkType: hard - "log-update@npm:^4.0.0": version: 4.0.0 resolution: "log-update@npm:4.0.0" @@ -44962,13 +44791,6 @@ __metadata: languageName: node linkType: hard -"mimic-fn@npm:^1.0.0": - version: 1.2.0 - resolution: "mimic-fn@npm:1.2.0" - checksum: 10c0/ad55214aec6094c0af4c0beec1a13787556f8116ed88807cf3f05828500f21f93a9482326bcd5a077ae91e3e8795b4e76b5b4c8bb12237ff0e4043a365516cba - languageName: node - linkType: hard - "mimic-fn@npm:^2.0.0, mimic-fn@npm:^2.1.0": version: 2.1.0 resolution: "mimic-fn@npm:2.1.0" @@ -45813,15 +45635,6 @@ __metadata: languageName: node linkType: hard -"native-fetch@npm:^4.0.2": - version: 4.0.2 - resolution: "native-fetch@npm:4.0.2" - peerDependencies: - undici: "*" - checksum: 10c0/e3b824721daaa628086d9dcd02e8eb12f0a6c5e13a1d182682bae238d80c9bbf3dfd6314a94692ebe20316aa354476804b4df148201d066b46fc552a5794cfab - languageName: node - linkType: hard - "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -46977,15 +46790,6 @@ __metadata: languageName: node linkType: hard -"onetime@npm:^2.0.0": - version: 2.0.1 - resolution: "onetime@npm:2.0.1" - dependencies: - mimic-fn: "npm:^1.0.0" - checksum: 10c0/b4e44a8c34e70e02251bfb578a6e26d6de6eedbed106cd78211d2fd64d28b6281d54924696554e4e966559644243753ac5df73c87f283b0927533d3315696215 - languageName: node - linkType: hard - "onetime@npm:^5.1.0, onetime@npm:^5.1.2": version: 5.1.2 resolution: "onetime@npm:5.1.2" @@ -47564,13 +47368,6 @@ __metadata: languageName: node linkType: hard -"p-map@npm:^2.0.0": - version: 2.1.0 - resolution: "p-map@npm:2.1.0" - checksum: 10c0/735dae87badd4737a2dd582b6d8f93e49a1b79eabbc9815a4d63a528d5e3523e978e127a21d784cccb637010e32103a40d2aaa3ab23ae60250b1a820ca752043 - languageName: node - linkType: hard - "p-map@npm:^4.0.0": version: 4.0.0 resolution: "p-map@npm:4.0.0" @@ -48243,13 +48040,20 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:8.4.2, path-to-regexp@npm:^8.0.0, path-to-regexp@npm:^8.4.0": +"path-to-regexp@npm:8.4.2, path-to-regexp@npm:^8.4.0": version: 8.4.2 resolution: "path-to-regexp@npm:8.4.2" checksum: 10c0/05b115c49b47ad252ce05faa32930f643f23769c68b8bcfe78ad833545140c48bbffb3266986d6c8d5db13a64cf12e07e0d72d9882cab830efeefa553533ebaf languageName: node linkType: hard +"path-to-regexp@npm:^8.0.0": + version: 8.3.0 + resolution: "path-to-regexp@npm:8.3.0" + checksum: 10c0/ee1544a73a3f294a97a4c663b0ce71bbf1621d732d80c9c9ed201b3e911a86cb628ebad691b9d40f40a3742fe22011e5a059d8eed2cf63ec2cb94f6fb4efe67c + languageName: node + linkType: hard + "path-to-regexp@npm:~0.1.12": version: 0.1.12 resolution: "path-to-regexp@npm:0.1.12" @@ -49126,7 +48930,7 @@ __metadata: languageName: node linkType: hard -"prettier@npm:2.8.8, prettier@npm:^2.0.0, prettier@npm:^2.8.0": +"prettier@npm:2.8.8, prettier@npm:^2.0.0, prettier@npm:^2.8.8": version: 2.8.8 resolution: "prettier@npm:2.8.8" bin: @@ -51928,16 +51732,6 @@ __metadata: languageName: node linkType: hard -"restore-cursor@npm:^2.0.0": - version: 2.0.0 - resolution: "restore-cursor@npm:2.0.0" - dependencies: - onetime: "npm:^2.0.0" - signal-exit: "npm:^3.0.2" - checksum: 10c0/f5b335bee06f440445e976a7031a3ef53691f9b7c4a9d42a469a0edaf8a5508158a0d561ff2b26a1f4f38783bcca2c0e5c3a44f927326f6694d5b44d7a4993e6 - languageName: node - linkType: hard - "restore-cursor@npm:^3.1.0": version: 3.1.0 resolution: "restore-cursor@npm:3.1.0" @@ -52055,17 +51849,6 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^2.6.3": - version: 2.7.1 - resolution: "rimraf@npm:2.7.1" - dependencies: - glob: "npm:^7.1.3" - bin: - rimraf: ./bin.js - checksum: 10c0/4eef73d406c6940927479a3a9dee551e14a54faf54b31ef861250ac815172bade86cc6f7d64a4dc5e98b65e4b18a2e1c9ff3b68d296be0c748413f092bb0dd40 - languageName: node - linkType: hard - "rimraf@npm:^3.0.0, rimraf@npm:^3.0.2": version: 3.0.2 resolution: "rimraf@npm:3.0.2" @@ -52472,7 +52255,7 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^6.3.3, rxjs@npm:^6.6.0": +"rxjs@npm:^6.6.0": version: 6.6.7 resolution: "rxjs@npm:6.6.7" dependencies: @@ -53793,13 +53576,6 @@ __metadata: languageName: node linkType: hard -"slice-ansi@npm:0.0.4": - version: 0.0.4 - resolution: "slice-ansi@npm:0.0.4" - checksum: 10c0/997d4cc73e34aa8c0f60bdb71701b16c062cc4acd7a95e3b10e8c05d790eb5e735d9b470270dc6f443b1ba21492db7ceb849d5c93011d1256061bf7ed7216c7a - languageName: node - linkType: hard - "slice-ansi@npm:^3.0.0": version: 3.0.0 resolution: "slice-ansi@npm:3.0.0" @@ -54579,16 +54355,6 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^2.1.1": - version: 2.1.1 - resolution: "string-width@npm:2.1.1" - dependencies: - is-fullwidth-code-point: "npm:^2.0.0" - strip-ansi: "npm:^4.0.0" - checksum: 10c0/e5f2b169fcf8a4257a399f95d069522f056e92ec97dbdcb9b0cdf14d688b7ca0b1b1439a1c7b9773cd79446cbafd582727279d6bfdd9f8edd306ea5e90e5b610 - languageName: node - linkType: hard - "string-width@npm:^5.0.0, string-width@npm:^5.0.1, string-width@npm:^5.1.2": version: 5.1.2 resolution: "string-width@npm:5.1.2" @@ -54716,15 +54482,6 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^4.0.0": - version: 4.0.0 - resolution: "strip-ansi@npm:4.0.0" - dependencies: - ansi-regex: "npm:^3.0.0" - checksum: 10c0/d75d9681e0637ea316ddbd7d4d3be010b1895a17e885155e0ed6a39755ae0fd7ef46e14b22162e66a62db122d3a98ab7917794e255532ab461bb0a04feb03e7d - languageName: node - linkType: hard - "strip-ansi@npm:^5.0.0, strip-ansi@npm:^5.2.0": version: 5.2.0 resolution: "strip-ansi@npm:5.2.0" @@ -55072,6 +54829,21 @@ __metadata: languageName: node linkType: hard +"subscriptions-transport-ws@npm:^0.9.16": + version: 0.9.19 + resolution: "subscriptions-transport-ws@npm:0.9.19" + dependencies: + backo2: "npm:^1.0.2" + eventemitter3: "npm:^3.1.0" + iterall: "npm:^1.2.1" + symbol-observable: "npm:^1.0.4" + ws: "npm:^5.2.0 || ^6.0.0 || ^7.0.0" + peerDependencies: + graphql: ">=0.10.0" + checksum: 10c0/6f2ade56865f0ba291d3ff82c79781b051c2374873bac853286fedfdbc05001b8c4018ab7cba44af667ead7f573e48d18892d58a8f9ca8d90dfb4bff5c125045 + languageName: node + linkType: hard + "sucrase@npm:^3.35.0": version: 3.35.0 resolution: "sucrase@npm:3.35.0" @@ -55143,13 +54915,6 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^2.0.0": - version: 2.0.0 - resolution: "supports-color@npm:2.0.0" - checksum: 10c0/570e0b63be36cccdd25186350a6cb2eaad332a95ff162fa06d9499982315f2fe4217e69dd98e862fbcd9c81eaff300a825a1fe7bf5cc752e5b84dfed042b0dda - languageName: node - linkType: hard - "supports-color@npm:^5.0.0, supports-color@npm:^5.3.0, supports-color@npm:^5.4.0, supports-color@npm:^5.5.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -55259,7 +55024,7 @@ __metadata: languageName: node linkType: hard -"symbol-observable@npm:^1.0.4, symbol-observable@npm:^1.1.0": +"symbol-observable@npm:^1.0.4": version: 1.2.0 resolution: "symbol-observable@npm:1.2.0" checksum: 10c0/009fee50798ef80ed4b8195048288f108b03de162db07493f2e1fd993b33fafa72d659e832b584da5a2427daa78e5a738fb2a9ab027ee9454252e0bedbcd1fdc @@ -56411,10 +56176,13 @@ __metadata: version: 0.0.0-use.local resolution: "twenty-client-sdk@workspace:packages/twenty-client-sdk" dependencies: - "@genql/cli": "npm:^3.0.3" + "@genql/runtime": "npm:^2.10.0" + "@types/lodash": "npm:^4.17.15" "@typescript/native-preview": "npm:^7.0.0-dev.20260116.1" esbuild: "npm:^0.28.0" graphql: "npm:^16.8.1" + lodash: "npm:^4.17.21" + prettier: "npm:^2.8.8" tsc-alias: "npm:^1.8.16" twenty-shared: "workspace:*" typescript: "npm:^5.9.3" @@ -56740,7 +56508,6 @@ __metadata: version: 0.0.0-use.local resolution: "twenty-sdk@workspace:packages/twenty-sdk" dependencies: - "@genql/cli": "npm:^3.0.3" "@prettier/sync": "npm:^0.5.2" "@sniptt/guards": "npm:^0.2.0" "@types/node": "npm:^24.0.0" @@ -57744,15 +57511,6 @@ __metadata: languageName: node linkType: hard -"undici@npm:^5.18.0": - version: 5.29.0 - resolution: "undici@npm:5.29.0" - dependencies: - "@fastify/busboy": "npm:^2.0.0" - checksum: 10c0/e4e4d631ca54ee0ad82d2e90e7798fa00a106e27e6c880687e445cc2f13b4bc87c5eba2a88c266c3eecffb18f26e227b778412da74a23acc374fca7caccec49b - languageName: node - linkType: hard - "unenv@npm:2.0.0-rc.24": version: 2.0.0-rc.24 resolution: "unenv@npm:2.0.0-rc.24" @@ -57762,6 +57520,13 @@ __metadata: languageName: node linkType: hard +"unfetch@npm:^4.2.0": + version: 4.2.0 + resolution: "unfetch@npm:4.2.0" + checksum: 10c0/a5c0a896a6f09f278b868075aea65652ad185db30e827cb7df45826fe5ab850124bf9c44c4dafca4bf0c55a0844b17031e8243467fcc38dd7a7d435007151f1b + languageName: node + linkType: hard + "unhead@npm:1.11.20": version: 1.11.20 resolution: "unhead@npm:1.11.20" @@ -60059,16 +59824,6 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^3.0.1": - version: 3.0.1 - resolution: "wrap-ansi@npm:3.0.1" - dependencies: - string-width: "npm:^2.1.1" - strip-ansi: "npm:^4.0.0" - checksum: 10c0/ad6fed8f242c26755badaf452da154122d0d862f8b7aab56e758466857f230efafdc5fbffca026650b947ac3fc0eb563df5c05b9e2190a52a4a68f4eef3d4555 - languageName: node - linkType: hard - "wrap-ansi@npm:^6.0.1, wrap-ansi@npm:^6.2.0": version: 6.2.0 resolution: "wrap-ansi@npm:6.2.0" @@ -60212,6 +59967,15 @@ __metadata: languageName: node linkType: hard +"ws@npm:^6.1.4": + version: 6.2.4 + resolution: "ws@npm:6.2.4" + dependencies: + async-limiter: "npm:~1.0.0" + checksum: 10c0/5c2b9474164f9cb68c7776a1d10b0461c186f3a69bffb1028fca33eba5ab7206a09173fb0b311d6c5a81c8cf148406f8deb0b7d899542ab8ca67407d99717dad + languageName: node + linkType: hard + "ws@npm:^8.12.0, ws@npm:^8.13.0, ws@npm:^8.18.0, ws@npm:^8.18.3, ws@npm:^8.19.0": version: 8.21.0 resolution: "ws@npm:8.21.0" @@ -60885,6 +60649,23 @@ __metadata: languageName: node linkType: hard +"zen-observable-ts@npm:^0.8.21": + version: 0.8.21 + resolution: "zen-observable-ts@npm:0.8.21" + dependencies: + tslib: "npm:^1.9.3" + zen-observable: "npm:^0.8.0" + checksum: 10c0/fe4a02f862b5f7e8ae0f86230c37b84c7d5611f5c206981afb4043e732d04cf7067a6cbe1ba82d20f18b735a3387937195a12542158a631d308ae3959a1d93c4 + languageName: node + linkType: hard + +"zen-observable@npm:^0.8.0": + version: 0.8.15 + resolution: "zen-observable@npm:0.8.15" + checksum: 10c0/71cc2f2bbb537300c3f569e25693d37b3bc91f225cefce251a71c30bc6bb3e7f8e9420ca0eb57f2ac9e492b085b8dfa075fd1e8195c40b83c951dd59c6e4fbf8 + languageName: node + linkType: hard + "zhead@npm:^2.2.4": version: 2.2.4 resolution: "zhead@npm:2.2.4"