security: vendor @genql/cli codegen to drop undici/native-fetch (#21339)

## 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.
This commit is contained in:
Charles Bochet
2026-06-08 20:46:45 +02:00
committed by GitHub
parent a0fb157899
commit 37b986aa4b
56 changed files with 2835 additions and 349 deletions
-1
View File
@@ -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",
+1 -1
View File
@@ -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",
@@ -1,2 +1,3 @@
dist
generated
src/generate/genql
+5 -2
View File
@@ -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:*",
@@ -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<string, unknown>) => Promise<any>;
mutation: (request: Record<string, unknown>) => Promise<any>;
};
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/);
});
});
@@ -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 = {
@@ -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 = {
@@ -0,0 +1,21 @@
MIT License
Copyright (c) Tommaso De Rossi, morse <beats.by.morse@gmail.com>
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.
@@ -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`.
@@ -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;
}
@@ -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);
};
@@ -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,
})
}
@@ -0,0 +1,2 @@
export { generate } from './main';
export { type Config } from './config';
@@ -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<void> => {
if (!config.output) {
throw new Error('`output` must be defined in the config');
}
const schema = await loadConfiguredSchema(config);
await writeClientFiles(config, schema);
};
@@ -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<fields extends ${requestTypeName(
queryType,
)}> = 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<fields extends ${requestTypeName(
mutationType,
)}> = 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<fields extends ${requestTypeName(
subscriptionType,
)}> = 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<R extends ${requestTypeName(queryType)}>(
request: R & { __name?: string },
): Promise<FieldsSelection<${queryType.name}, R>>
`
}
if (mutationType) {
interfaceContent += `
mutation<R extends ${requestTypeName(mutationType)}>(
request: R & { __name?: string },
): Promise<FieldsSelection<${mutationType.name}, R>>
`
}
// TODO add subscription client again
// if (subscriptionType) {
// interfaceContent += `
// subscription<R extends ${requestTypeName(subscriptionType)}>(
// request: R & { __name?: string },
// ): Observable<FieldsSelection<${subscriptionType.name}, R>>
// `
// }
return `
export interface Client {
${interfaceContent}
}
`
}
@@ -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('')
}
}
@@ -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<any, any, any>) =>
comment({
deprecated: field.deprecationReason,
text: field.description,
})
export const argumentComment = (arg: GraphQLArgument | GraphQLInputField) =>
comment({
text: arg.description,
})
@@ -0,0 +1,11 @@
// @ts-nocheck
export const excludedTypes = [
'__Schema',
'__Type',
'__TypeKind',
'__Field',
'__InputValue',
'__EnumValue',
'__Directive',
'__DirectiveLocation',
]
@@ -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}`
}
@@ -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<any>).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)
@@ -0,0 +1,16 @@
// @ts-nocheck
export function sortKeys(obj: Record<any, any>): Record<any, any> {
obj = obj || {}
const ordered = {}
Object.keys(obj)
.sort()
// .reverse()
.forEach(function(key) {
ordered[key] = obj[key]
})
return ordered
}
export function intersection<T>(a: T[][]): T[] {
return a.reduce((p, c) => p.filter((e) => c.includes(e)))
}
@@ -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(',')}}`)
}
@@ -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<any, any, any>) => {
let fields = field.args
.map(
(a) =>
`${argumentComment(a)}${a.name}${renderTyping(
a.type,
false,
true,
)}`,
)
.join(', ')
return `{${fields}}`
}
@@ -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 ''
}
@@ -0,0 +1,7 @@
// @ts-nocheck
import { GraphQLNamedType } from 'graphql'
export const requestTypeName = (type?: GraphQLNamedType) => {
if (!type) return ''
return `${type.name}GenqlSelection`
}
@@ -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}`,
)
}
@@ -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(' | ')}`)
}
@@ -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
// }`,
// )
// }
@@ -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}`,
)
}
@@ -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 ''
}
@@ -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}}`
}
@@ -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}`,
// )
// }
// }
@@ -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
// }`,
// )
// }
@@ -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))
}
@@ -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]))
}
}
}
@@ -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<string> = Object.keys(type.getFields()).reduce<
FieldMap<string>
>((r, f) => {
const field = type.getFields()[f]
const namedType = getNamedType(field.type)
const fieldObj: Field<string> = { type: namedType.name }
r[f] = fieldObj
const args: readonly GraphQLArgument[] =
(field as GraphQLField<any, any>).args || []
if (args.length > 0) {
fieldObj.args = args.reduce<ArgMap<string>>((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
}
@@ -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<string> = {
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<string>,
): CompressedTypeMap<number> {
const nameToIndex: Record<string, number> = 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<number> => {
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<number> = [
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<number>
}),
)
}
return {
[f]: res,
}
},
),
)
return {
[k]: {
...fields,
},
}
}),
)
return {
scalars,
types,
}
}
@@ -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<string> => ({})
@@ -0,0 +1,7 @@
// @ts-nocheck
export function isEmpty(x) {
if (!x) {
return true
}
return Object.keys(x).length === 0
}
@@ -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<string> = types.reduce<FieldMap<string>>((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
}
@@ -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 },
];
@@ -0,0 +1,270 @@
import type { GraphqlOperation } from './generateGraphqlOperation'
import { GenqlError } from './error'
type Variables = Record<string, any>
type QueryError = Error & {
message: string
locations?: Array<{
line: number
column: number
}>
path?: any
rid: string
details?: Record<string, any>
}
type Result = {
data: Record<string, any>
errors: Array<QueryError>
}
type Fetcher = (
batchedQuery: GraphqlOperation | Array<GraphqlOperation>,
) => Promise<Array<Result>>
type Options = {
batchInterval?: number
shouldBatch?: boolean
maxBatchSize?: number
}
type Queue = Array<{
request: GraphqlOperation
resolve: (...args: Array<any>) => any
reject: (...args: Array<any>) => 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<Result> {
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<Result>((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<Array<Result>>} 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<Result> {
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<Result>((resolve, reject) => {
const client = new QueryBatcher(this.fetcher, this._options)
client._queue = [
{
request,
resolve,
reject,
},
]
dispatchQueue(client, options)
})
return promise
}
}
@@ -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<HeadersInit>)
export type BaseFetcher = (
operation: GraphqlOperation | GraphqlOperation[],
) => Promise<ExecutionResult | ExecutionResult[]>
export type ClientOptions = Omit<RequestInit, 'body' | 'headers'> & {
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
}
@@ -0,0 +1,28 @@
export class GenqlError extends Error {
errors: Array<GraphqlError> = []
/**
* 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<string, any>
}
@@ -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<any>
}
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),
)
}
}
@@ -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
}
@@ -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,
}
@@ -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<number>,
): LinkedTypeMap => {
const indexToName: Record<number, string> = Object.assign(
{},
...Object.keys(typeMap.types).map((k, i) => ({ [i]: k })),
)
let intermediaryTypeMap = Object.assign(
{},
...Object.keys(typeMap.types || {}).map(
(k): Record<string, LinkedType> => {
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
}
@@ -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<SRC extends Anify<DST> | undefined, DST> = {
scalar: SRC
union: Handle__isUnion<SRC, DST>
object: HandleObject<SRC, DST>
array: SRC extends Nil
? never
: SRC extends (infer T)[]
? Array<FieldsSelection<T, DST>>
: never
__scalar: Handle__scalar<SRC, DST>
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<SRC extends Anify<DST>, 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<SRC[Key]>,
NonNullable<DST[Key]>
>
: SRC[Key]
},
Exclude<keyof DST, FieldsToRemove>
// {
// // remove falsy values
// [Key in keyof DST]: DST[Key] extends false | 0 ? never : Key
// }[keyof DST]
>
type Handle__scalar<SRC extends Anify<DST>, 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], DST[Key]>
: 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<SRC extends Anify<DST>, DST> = SRC extends Nil
? never
: Omit<SRC, FieldsToRemove> // just return the union type
type Scalar = string | number | Date | boolean | null | undefined
type Anify<T> = { [P in keyof T]?: any }
type FieldsToRemove = '__isUnion' | '__scalar' | '__name' | '__args'
type Nil = undefined | null
@@ -0,0 +1,68 @@
export interface ExecutionResult<TData = { [key: string]: any }> {
errors?: Array<Error>
data?: TData | null
}
export interface ArgMap<keyType = number> {
[arg: string]: [keyType, string] | [keyType] | undefined
}
export type CompressedField<keyType = number> = [
type: keyType,
args?: ArgMap<keyType>,
]
export interface CompressedFieldMap<keyType = number> {
[field: string]: CompressedField<keyType> | undefined
}
export type CompressedType<keyType = number> = CompressedFieldMap<keyType>
export interface CompressedTypeMap<keyType = number> {
scalars: Array<keyType>
types: {
[type: string]: CompressedType<keyType> | undefined
}
}
// normal types
export type Field<keyType = number> = {
type: keyType
args?: ArgMap<keyType>
}
export interface FieldMap<keyType = number> {
[field: string]: Field<keyType> | undefined
}
export type Type<keyType = number> = FieldMap<keyType>
export interface TypeMap<keyType = number> {
scalars: Array<keyType>
types: {
[type: string]: Type<keyType> | 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
}
@@ -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<void> => {
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'),
);
}
@@ -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<GraphQLSchema> => {
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;
};
@@ -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)
}
})
}
@@ -58,6 +58,7 @@ export default defineConfig(() => {
'node:fs/promises',
'node:fs',
'node:path',
'node:os',
],
output: [
{
-1
View File
@@ -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",
+123 -342
View File
@@ -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"