snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "import", "unicorn"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules", "dist", "generated", "src/generate/genql"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": "off",
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
"no-undef": "off",
"no-unused-vars": "off",
"no-redeclare": "off",
"import/no-duplicates": "error",
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
]
}
}

View File

@@ -0,0 +1,3 @@
dist
generated
src/generate/genql

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023-present Twenty.com, PBC
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.

View File

@@ -0,0 +1,71 @@
{
"name": "twenty-client-sdk",
"version": "2.42.0",
"sideEffects": false,
"license": "MIT",
"scripts": {
"build": "npx rimraf dist && npx vite build && npx vite build -c vite.metadata.config.ts && tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist"
},
"exports": {
"./core": {
"types": "./dist/core/index.d.ts",
"import": "./dist/core.mjs",
"require": "./dist/core.cjs"
},
"./metadata": {
"types": "./dist/metadata/index.d.ts",
"import": "./dist/metadata.mjs",
"require": "./dist/metadata.cjs"
},
"./rest": {
"types": "./dist/rest/index.d.ts",
"import": "./dist/rest.mjs",
"require": "./dist/rest.cjs"
},
"./generate": {
"types": "./dist/generate/index.d.ts",
"import": "./dist/generate.mjs",
"require": "./dist/generate.cjs"
}
},
"typesVersions": {
"*": {
"core": [
"dist/core/index.d.ts"
],
"metadata": [
"dist/metadata/index.d.ts"
],
"rest": [
"dist/rest/index.d.ts"
],
"generate": [
"dist/generate/index.d.ts"
]
}
},
"files": [
"dist"
],
"dependencies": {
"@genql/runtime": "^2.10.0",
"esbuild": "^0.28.1",
"graphql": "^16.8.1",
"lodash": "^4.17.21",
"prettier": "^3.8.3"
},
"devDependencies": {
"@types/lodash": "^4.17.24",
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"tsc-alias": "^1.8.16",
"twenty-shared": "workspace:*",
"typescript": "^5.9.3",
"vite": "^8.0.0",
"vite-plugin-dts": "^4.5.4",
"vitest": "^4.1.0"
},
"engines": {
"node": "^24.5.0",
"yarn": "^4.0.2"
}
}

View File

@@ -0,0 +1,49 @@
{
"name": "twenty-client-sdk",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/twenty-client-sdk/src",
"projectType": "library",
"tags": ["scope:client-sdk"],
"targets": {
"build": {
"executor": "nx:run-commands",
"cache": true,
"inputs": ["production", "^production"],
"dependsOn": ["^build"],
"outputs": ["{projectRoot}/dist"],
"options": {
"cwd": "{projectRoot}",
"commands": [
"npx rimraf dist && npx vite build && npx vite build -c vite.metadata.config.ts",
"tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist"
],
"parallel": false
}
},
"generate-metadata-client": {
"executor": "nx:run-commands",
"cache": false,
"dependsOn": ["^build"],
"outputs": ["{projectRoot}/src/metadata/generated"],
"options": {
"cwd": "packages/twenty-client-sdk",
"command": "tsx scripts/generate-metadata-client.ts"
}
},
"test": {
"executor": "@nx/vitest:test",
"dependsOn": ["^build"],
"options": {
"config": "{projectRoot}/vitest.config.ts"
},
"configurations": {
"ci": {
"watch": false
}
}
},
"set-local-version": {},
"typecheck": {},
"lint": {}
}
}

View File

@@ -0,0 +1,70 @@
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { buildClientSchema, getIntrospectionQuery, printSchema } from 'graphql';
import { generateMetadataClient } from '../src/generate/generate-metadata-client';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const TEMPLATE_PATH = path.resolve(
__dirname,
'..',
'src',
'generate',
'twenty-client-template.ts',
);
const introspectSchema = async (url: string): Promise<string> => {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
// Deprecated input fields are omitted from introspection by default, which
// would drop them from the client and break callers a deprecation is meant
// to keep working
body: JSON.stringify({
query: getIntrospectionQuery({ inputValueDeprecation: true }),
}),
});
const json = await response.json();
if (json.errors) {
throw new Error(
`GraphQL introspection errors: ${JSON.stringify(json.errors)}`,
);
}
return printSchema(buildClientSchema(json.data));
};
const main = async () => {
const serverUrl = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
const schema = await introspectSchema(`${serverUrl}/metadata`);
const clientWrapperTemplateSource = await readFile(TEMPLATE_PATH, 'utf-8');
const outputPath = path.resolve(
__dirname,
'..',
'src',
'metadata',
'generated',
);
await generateMetadataClient({
schema,
outputPath,
clientWrapperTemplateSource,
});
console.log(`Metadata client generated at ${outputPath}`);
};
main().catch((error) => {
console.error('Failed to generate metadata client:', error);
process.exit(1);
});

View File

@@ -0,0 +1,17 @@
// Stub — this file is replaced by the generated client when the app
// is installed or sync on a Twenty instance.
// Do not edit manually.
import { type TwentyClientOptions } from '../../shared/twenty-client-options.type';
export class CoreApiClient {
query: any;
mutation: any;
upload: any;
constructor(_options?: TwentyClientOptions) {
throw new Error(
'CoreApiClient was not generated. ' +
'Install this app on a Twenty instance or run `yarn twenty dev`.',
);
}
}

View File

@@ -0,0 +1,2 @@
// Stub — overwritten by `twenty dev:build` or `twenty dev`
export type CoreSchema = {};

View File

@@ -0,0 +1,2 @@
export { CoreApiClient } from './generated/index';
export * as CoreSchema from './generated/schema';

View File

@@ -0,0 +1,584 @@
import { readFileSync } from 'node:fs';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { transform } from 'esbuild';
import {
afterAll,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
import { buildClientWrapperSource } from '../client-wrapper';
const twentyClientTemplateSource = readFileSync(
join(__dirname, '..', 'twenty-client-template.ts'),
'utf-8',
);
type TwentyClassType = new (options?: {
url?: string;
fetch?: typeof globalThis.fetch;
}) => {
query: (request: Record<string, unknown>) => Promise<unknown>;
uploadFile: (
fileBuffer: Buffer,
filename: string,
contentType: string,
fieldMetadataUniversalIdentifier: string,
) => Promise<{
id: string;
path: string;
size: number;
createdAt: string;
url: string;
}>;
};
const stubGeneratedIndexSource = `
export type QueryGenqlSelection = Record<string, unknown>
export type MutationGenqlSelection = Record<string, unknown>
export type GraphqlOperation = Record<string, unknown>
export type ClientOptions = {
url?: string
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>)
fetcher?: (operation: GraphqlOperation | GraphqlOperation[]) => Promise<unknown>
fetch?: typeof globalThis.fetch
batch?: unknown
}
export type Client = {
query: (request: QueryGenqlSelection & { __name?: string }) => Promise<unknown>
mutation: (
request: MutationGenqlSelection & { __name?: string },
) => Promise<unknown>
}
export class GenqlError extends Error {
constructor(
public readonly errors: unknown,
public readonly data: unknown,
) {
super('GenqlError')
}
}
export const createClient = (options: ClientOptions): Client => {
return {
query: (request) => {
return options.fetcher?.({
query: 'query',
variables: request,
})
},
mutation: (request) => {
return options.fetcher?.({
query: 'mutation',
variables: request,
})
},
}
}
`;
const createJsonResponse = ({
body,
status = 200,
statusText = 'OK',
}: {
body: unknown;
status?: number;
statusText?: string;
}) =>
new Response(JSON.stringify(body), {
status,
statusText,
headers: { 'Content-Type': 'application/json' },
});
const getAuthorizationHeaderValue = (requestInit: RequestInit | undefined) => {
return new Headers(requestInit?.headers).get('Authorization');
};
describe('Generated client wrapper auth behavior', () => {
let temporaryDir: string;
let TwentyClass: TwentyClassType;
beforeAll(async () => {
temporaryDir = await mkdtemp(join(tmpdir(), 'twenty-generated-client-'));
const wrapperSource = buildClientWrapperSource(twentyClientTemplateSource, {
apiClientName: 'MetadataApiClient',
defaultUrl: '`${process.env.TWENTY_API_URL}/metadata`',
includeUploadFile: true,
});
const fullSource = stubGeneratedIndexSource + wrapperSource;
const transpiledModule = await transform(fullSource, {
loader: 'ts',
format: 'esm',
target: 'es2022',
});
const outputPath = join(temporaryDir, 'index.mjs');
await writeFile(outputPath, transpiledModule.code);
const generatedModule = await import(
`${pathToFileURL(outputPath).href}?t=${Date.now()}`
);
TwentyClass = generatedModule.MetadataApiClient as TwentyClassType;
});
beforeEach(() => {
delete process.env.TWENTY_APP_ACCESS_TOKEN;
delete process.env.TWENTY_APP_APPLICATION_ACCESS_TOKEN;
delete process.env.TWENTY_API_KEY;
delete (globalThis as Record<string, unknown>)
.frontComponentHostCommunicationApi;
});
afterAll(async () => {
if (temporaryDir) {
await rm(temporaryDir, { recursive: true, force: true });
}
});
const runQueryCapturingAuthorization = async (
clientOptions: Record<string, unknown>,
) => {
const capturedAuthorizationHeaders: string[] = [];
const fetchMock = vi.fn(
async (_url: string | URL | Request, requestInit?: RequestInit) => {
const authorizationHeaderValue =
getAuthorizationHeaderValue(requestInit);
if (authorizationHeaderValue) {
capturedAuthorizationHeaders.push(authorizationHeaderValue);
}
return createJsonResponse({
body: { data: { record: { id: 'record-id' } } },
});
},
);
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
fetch: fetchMock as unknown as typeof globalThis.fetch,
...clientOptions,
});
await twentyClient.query({ record: { id: true } });
return capturedAuthorizationHeaders;
};
it('should act as the triggering person by default', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'delegated-token';
process.env.TWENTY_APP_APPLICATION_ACCESS_TOKEN = 'application-token';
expect(await runQueryCapturingAuthorization({})).toEqual([
'Bearer delegated-token',
]);
});
it('should act as the application when asked to', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'delegated-token';
process.env.TWENTY_APP_APPLICATION_ACCESS_TOKEN = 'application-token';
expect(
await runQueryCapturingAuthorization({ runAs: 'application' }),
).toEqual(['Bearer application-token']);
});
it('should act as the application when nobody triggered the run', async () => {
// The runtime points TWENTY_APP_ACCESS_TOKEN at the application in a cron
// schedule or install hook, so the default client keeps working there.
process.env.TWENTY_APP_ACCESS_TOKEN = 'application-token';
process.env.TWENTY_APP_APPLICATION_ACCESS_TOKEN = 'application-token';
expect(await runQueryCapturingAuthorization({})).toEqual([
'Bearer application-token',
]);
});
it('should still serve a run nobody triggered when asked for the application', async () => {
process.env.TWENTY_APP_APPLICATION_ACCESS_TOKEN = 'application-token';
expect(
await runQueryCapturingAuthorization({ runAs: 'application' }),
).toEqual(['Bearer application-token']);
});
it('should build outside a logic function run', () => {
expect(
() => new TwentyClass({ url: 'https://example.com/graphql' }),
).not.toThrow();
});
it('uses TWENTY_APP_ACCESS_TOKEN before TWENTY_API_KEY', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'application-token';
process.env.TWENTY_API_KEY = 'api-key-token';
const capturedAuthorizationHeaders: string[] = [];
const fetchMock = vi.fn(
async (_url: string | URL | Request, requestInit?: RequestInit) => {
const authorizationHeaderValue =
getAuthorizationHeaderValue(requestInit);
if (authorizationHeaderValue) {
capturedAuthorizationHeaders.push(authorizationHeaderValue);
}
return createJsonResponse({
body: { data: { record: { id: 'record-id' } } },
});
},
);
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
fetch: fetchMock as unknown as typeof globalThis.fetch,
});
await twentyClient.query({ record: { id: true } });
expect(capturedAuthorizationHeaders).toEqual(['Bearer application-token']);
});
it('falls back to TWENTY_API_KEY when TWENTY_APP_ACCESS_TOKEN is absent', async () => {
process.env.TWENTY_API_KEY = 'legacy-api-key-token';
const capturedAuthorizationHeaders: string[] = [];
const fetchMock = vi.fn(
async (_url: string | URL | Request, requestInit?: RequestInit) => {
const authorizationHeaderValue =
getAuthorizationHeaderValue(requestInit);
if (authorizationHeaderValue) {
capturedAuthorizationHeaders.push(authorizationHeaderValue);
}
return createJsonResponse({
body: { data: { record: { id: 'record-id' } } },
});
},
);
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
fetch: fetchMock as unknown as typeof globalThis.fetch,
});
await twentyClient.query({ record: { id: true } });
expect(capturedAuthorizationHeaders).toEqual([
'Bearer legacy-api-key-token',
]);
});
it('refreshes and retries once after auth error when refresh callback is available', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'stale-token';
process.env.TWENTY_API_KEY = 'legacy-api-key-token';
const requestAccessTokenRefresh = vi
.fn<() => Promise<string>>()
.mockResolvedValue('fresh-token');
(globalThis as Record<string, unknown>).frontComponentHostCommunicationApi =
{
requestAccessTokenRefresh,
};
const fetchMock = vi.fn(
async (_url: string | URL | Request, requestInit?: RequestInit) => {
const authorizationHeaderValue =
getAuthorizationHeaderValue(requestInit);
if (authorizationHeaderValue === 'Bearer stale-token') {
return createJsonResponse({
body: {
errors: [
{
extensions: { code: 'UNAUTHENTICATED' },
message: 'Unauthorized',
},
],
},
});
}
return createJsonResponse({
body: { data: { record: { id: 'record-id' } } },
});
},
);
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
fetch: fetchMock as unknown as typeof globalThis.fetch,
});
const queryResult = await twentyClient.query({ record: { id: true } });
expect(queryResult).toEqual({ data: { record: { id: 'record-id' } } });
expect(requestAccessTokenRefresh).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(process.env.TWENTY_APP_ACCESS_TOKEN).toBe('fresh-token');
expect(process.env.TWENTY_API_KEY).toBe('legacy-api-key-token');
});
it('deduplicates concurrent token refresh requests', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'stale-token';
const requestAccessTokenRefresh = vi
.fn<() => Promise<string>>()
.mockImplementation(async () => {
await Promise.resolve();
return 'fresh-token';
});
(globalThis as Record<string, unknown>).frontComponentHostCommunicationApi =
{
requestAccessTokenRefresh,
};
const fetchMock = vi.fn(
async (_url: string | URL | Request, requestInit?: RequestInit) => {
const authorizationHeaderValue =
getAuthorizationHeaderValue(requestInit);
if (authorizationHeaderValue === 'Bearer stale-token') {
return createJsonResponse({
body: {
errors: [
{
extensions: { code: 'UNAUTHENTICATED' },
message: 'Unauthorized',
},
],
},
});
}
return createJsonResponse({
body: { data: { record: { id: 'record-id' } } },
});
},
);
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
fetch: fetchMock as unknown as typeof globalThis.fetch,
});
await Promise.all([
twentyClient.query({ record: { id: true } }),
twentyClient.query({ record: { id: true } }),
]);
expect(requestAccessTokenRefresh).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledTimes(4);
});
it('retries uploadFile once after 401 when refresh callback is available', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'stale-token';
const requestAccessTokenRefresh = vi
.fn<() => Promise<string>>()
.mockResolvedValue('fresh-token');
(globalThis as Record<string, unknown>).frontComponentHostCommunicationApi =
{
requestAccessTokenRefresh,
};
const fetchMock = vi.fn(
async (_url: string | URL | Request, requestInit?: RequestInit) => {
const authorizationHeaderValue =
getAuthorizationHeaderValue(requestInit);
if (authorizationHeaderValue === 'Bearer stale-token') {
return createJsonResponse({
body: { message: 'Unauthorized' },
status: 401,
statusText: 'Unauthorized',
});
}
return createJsonResponse({
body: {
data: {
uploadFilesFieldFileByUniversalIdentifier: {
id: 'uploaded-file-id',
path: 'test/path.txt',
size: 10,
createdAt: '2026-02-24T00:00:00.000Z',
url: 'https://example.com/test/path.txt',
},
},
},
});
},
);
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
fetch: fetchMock as unknown as typeof globalThis.fetch,
});
const uploadResult = await twentyClient.uploadFile(
Buffer.from('content'),
'test.txt',
'text/plain',
'field-uuid',
);
expect(uploadResult.id).toBe('uploaded-file-id');
expect(requestAccessTokenRefresh).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledTimes(2);
});
it('bubbles auth error when refresh callback throws', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'stale-token';
const requestAccessTokenRefresh = vi
.fn<() => Promise<string>>()
.mockRejectedValue(new Error('refresh failed'));
(globalThis as Record<string, unknown>).frontComponentHostCommunicationApi =
{
requestAccessTokenRefresh,
};
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined);
const fetchMock = vi.fn(async () => {
return createJsonResponse({
body: { message: 'Unauthorized' },
status: 401,
statusText: 'Unauthorized',
});
});
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
fetch: fetchMock as unknown as typeof globalThis.fetch,
});
await expect(twentyClient.query({ record: { id: true } })).rejects.toThrow(
'Unauthorized',
);
expect(requestAccessTokenRefresh).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Twenty client: token refresh failed',
expect.any(Error),
);
consoleErrorSpy.mockRestore();
});
it('bubbles auth error when refresh callback returns an empty token', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'stale-token';
const requestAccessTokenRefresh = vi
.fn<() => Promise<string>>()
.mockResolvedValue('');
(globalThis as Record<string, unknown>).frontComponentHostCommunicationApi =
{
requestAccessTokenRefresh,
};
const fetchMock = vi.fn(async () => {
return createJsonResponse({
body: { message: 'Unauthorized' },
status: 401,
statusText: 'Unauthorized',
});
});
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
fetch: fetchMock as unknown as typeof globalThis.fetch,
});
await expect(twentyClient.query({ record: { id: true } })).rejects.toThrow(
'Unauthorized',
);
expect(requestAccessTokenRefresh).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('bubbles auth error without retry when refresh callback is unavailable', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'stale-token';
const fetchMock = vi.fn(async () => {
return createJsonResponse({
body: { message: 'Unauthorized' },
status: 401,
statusText: 'Unauthorized',
});
});
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
fetch: fetchMock as unknown as typeof globalThis.fetch,
});
await expect(twentyClient.query({ record: { id: true } })).rejects.toThrow(
'Unauthorized',
);
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('calls global fetch with globalThis binding when no custom fetch is provided', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'application-token';
const originalFetch = globalThis.fetch;
const capturedFetchThisValues: unknown[] = [];
const fetchMock = vi.fn(function (
this: unknown,
_url: string | URL | Request,
_requestInit?: RequestInit,
) {
capturedFetchThisValues.push(this);
return Promise.resolve(
createJsonResponse({
body: { data: { record: { id: 'record-id' } } },
}),
);
}) as unknown as typeof globalThis.fetch;
globalThis.fetch = fetchMock;
try {
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
});
await twentyClient.query({ record: { id: true } });
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(capturedFetchThisValues).toEqual([globalThis]);
} finally {
globalThis.fetch = originalFetch;
}
});
});

View File

@@ -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/);
});
});

View File

@@ -0,0 +1,31 @@
import { type Expect, type HasAllProperties } from 'twenty-shared/testing';
import { describe, expect, it } from 'vitest';
import { CoreApiClient } from '../../core/generated/index';
import { type TwentyGeneratedClientOptions } from '../twenty-client-template';
type StubOptions = NonNullable<ConstructorParameters<typeof CoreApiClient>[0]>;
type GeneratedOptions = NonNullable<TwentyGeneratedClientOptions>;
type _StubHasEveryGeneratedOption = Expect<
HasAllProperties<StubOptions, GeneratedOptions>
>;
type _GeneratedHasEveryStubOption = Expect<
HasAllProperties<GeneratedOptions, StubOptions>
>;
describe('CoreApiClient stub', () => {
it('accepts the full generated option set and still refuses to run', () => {
expect(
() =>
new CoreApiClient({
url: 'https://example.test/graphql',
headers: { Authorization: 'Bearer token' },
fetcher: async () => ({}),
fetch: globalThis.fetch,
batch: false,
runAs: 'application',
}),
).toThrow('CoreApiClient was not generated.');
});
});

View File

@@ -0,0 +1,47 @@
# Ported genql 3.0.5 test suite
Port of the test suite of [`remorses/genql`](https://github.com/remorses/genql)
at `@genql/cli@3.0.5` (commit `4a547db46a9a614cc2b5958e28674af351898464`), the
version the codegen in `../../genql` was vendored from. It protects the
vendored engine's behavior; the engine's output bytes are pinned by
`../genql-engine-output.test.ts` and by the checked-in `fixture/generated/`
client (`generated-fixture-drift.test.ts`).
| Here | Upstream |
| --- | --- |
| `fixture/schema.graphql` | `integration-tests/schema.graphql` (byte-verbatim) |
| `fixture/generated/` | output of the vendored engine for that schema, checked in so type-level assertions compile; pinned by `generated-fixture-drift.test.ts` |
| `generate-queries.test.ts` | `integration-tests/tests/simple.ts` |
| `execute-queries.test.ts` | `integration-tests/tests/execution.ts` |
| `type-selection.test.ts` | `cli/src/typeSelection.test.ts` |
| `type-map.test.ts` | `cli/src/render/typeMap/index.test.ts` |
| `render-typing.test.ts` | `cli/src/render/common/__tests__/renderTyping.test.ts` |
| `comment.test.ts` | `cli/src/render/common/__tests__/comment.test.ts` |
| `render-context.test.ts` | `cli/src/render/common/__tests__/RenderContext.test.ts` |
| `render-schema.test.ts` | `cli/src/render/schema/renderSchema.test.ts` |
| `render-test-helpers.ts` | `cli/src/testHelpers/render.ts` |
Not ported: `parse.test.ts` and `printer.test.ts` (they cover CLI/printer code
Twenty did not vendor) and the subscription suite (already skipped upstream).
## Adaptations
Selections, resolvers, assertions and test names are upstream's. What changed:
- **mocha/sucrase/snap-shot-it → vitest.** Snapshots are vitest snapshots.
- **apollo-server (v3, deprecated) → graphql-js execution.** `execution.ts`
booted a real HTTP server per test; `execute-queries.test.ts` runs the same
resolvers through `graphql()` behind an injected `fetch`/`fetcher`, which
exercises the identical client code path with no network and no new
dependency.
- **tsd `expectType` → vitest `expectTypeOf`, strengthened to exact.**
Upstream's CI did typecheck its integration tests (its `gen` step ran
`tsc --noEmit` over tests plus a freshly generated client), but it never ran
the tsd CLI — and under plain tsc, tsd's `expectType<T>(value)` degrades to
an assignability check. Several assertions were therefore looser than the
real types. The exact assertions here pin what the 3.0.5 engine actually
generates (fields absent from a response are `undefined`, never `null`;
`__typename` is a literal type) and are enforced by the package typecheck.
- **`type-map.test.ts` assertions enabled.** Upstream ran them in
output-logging mode with stale expected values; the values here match the
engine's actual output.

View File

@@ -0,0 +1,191 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`generate queries > do not fetch falsy fields 1`] = `
"subscription {
user {
name
}
}
"
`;
exports[`generate queries > do not fetch falsy fields with __scalar 1`] = `
"subscription {
user {
...f1
}
}
fragment f1 on User {
name
__typename
}
"
`;
exports[`generate queries > many 1`] = `
"query ($v1: String!, $v2: String) {
repository(name: $v1, owner: $v2) {
createdAt
forks {
edges {
cursor
node {
...f1
}
}
}
}
user {
...f2
}
}
fragment f1 on Fork {
name
number
__typename
}
fragment f2 on User {
name
common
__typename
}
"
`;
exports[`generate queries > optional arg 1`] = `
"query {
optionalArgs {
createdAt
forks {
edges {
cursor
node {
...f1
}
}
}
}
}
fragment f1 on Fork {
name
number
__typename
}
"
`;
exports[`generate queries > query 1`] = `
"query ($v1: String!, $v2: String) {
repository(name: $v1, owner: $v2) {
createdAt
forks {
edges {
cursor
node {
...f1
}
}
}
}
}
fragment f1 on Fork {
name
number
__typename
}
"
`;
exports[`generate queries > recursive type 1`] = `
"query {
recursiveType {
value
recurse {
recurse {
value
recurse {
recurse {
...f3
}
...f2
}
}
...f1
}
}
}
fragment f1 on RecursiveType {
value
__typename
}
fragment f2 on RecursiveType {
value
__typename
}
fragment f3 on RecursiveType {
value
__typename
}
"
`;
exports[`generate queries > recursive type with args 1`] = `
"query ($v1: [String!], $v2: Int, $v3: Int, $v4: Int) {
recursiveType(requiredVal: $v1) {
value
recurse(arg: $v2) {
recurse(arg: $v3) {
value
recurse(arg: $v4) {
recurse {
...f3
}
...f2
}
}
...f1
}
}
}
fragment f1 on RecursiveType {
value
__typename
}
fragment f2 on RecursiveType {
value
__typename
}
fragment f3 on RecursiveType {
value
__typename
}
"
`;
exports[`generate queries > subscriptions 1`] = `
"subscription {
user {
...f1
}
}
fragment f1 on User {
name
common
__typename
}
"
`;
exports[`generate queries > use __name operation name 1`] = `
"subscription SomeName {
user {
...f1
}
}
fragment f1 on User {
name
common
__typename
}
"
`;

View File

@@ -0,0 +1,42 @@
import { expect, test } from 'vitest';
import { comment } from '../../genql/render/common/comment';
// Port of remorses/genql@v3.0.5
// cli/src/render/common/__tests__/comment.test.ts, with common-tags'
// stripIndent replaced by explicit expected strings.
test('deprecated', () => {
expect(comment({ deprecated: 'deprecation reason' })).toBe(
'\n/** @deprecated deprecation reason */\n',
);
});
test('deprecated multiline', () => {
expect(comment({ deprecated: 'deprecation\nreason\nmultiline' })).toBe(
'\n/** @deprecated deprecation reason multiline */\n',
);
});
test('single line', () => {
expect(comment({ text: 'single line' })).toBe('\n/** single line */\n');
});
test('single line deprecated', () => {
expect(
comment({ text: 'single line', deprecated: 'deprecation reason' }),
).toBe('\n/**\n * @deprecated deprecation reason\n * single line\n */\n');
});
test('multiline', () => {
expect(comment({ text: 'multiline\ntext' })).toBe(
'\n/**\n * multiline\n * text\n */\n',
);
});
test('multiline deprecated', () => {
expect(
comment({ text: 'multiline\ntext', deprecated: 'deprecation reason' }),
).toBe(
'\n/**\n * @deprecated deprecation reason\n * multiline\n * text\n */\n',
);
});

View File

@@ -0,0 +1,459 @@
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
buildSchema,
graphql,
type ExecutionResult,
type GraphQLSchema,
} from 'graphql';
import { describe, expect, expectTypeOf, it, vi } from 'vitest';
import {
createClient,
everything,
GenqlError,
isHouse,
type Account,
type Point,
type User,
} from './fixture/generated';
import { type GraphqlOperation } from './fixture/generated/runtime';
// Port of remorses/genql@v3.0.5 integration-tests/tests/execution.ts ("execute
// queries"). Upstream booted an apollo-server (v3, now deprecated) per test;
// here the same resolvers run through graphql-js `graphql()` behind an
// injected `fetch`/`fetcher`, which exercises the identical client code path
// (operation building, POST body, response parsing, batching, errors) without
// a network or any extra dependency. graphql-js resolves the abstract types
// (Account, Point, GenericError) from the `__typename` each resolver returns,
// as apollo did. Selections, assertions and test names are upstream's; tsd's
// `expectType` becomes `expectTypeOf`. The skipped upstream subscription suite
// (already skipped there) is not ported.
//
// One deliberate correction: upstream typechecked this file with plain tsc
// (never the tsd CLI), and under plain tsc tsd's `expectType<T>(value)` is
// only an assignability check — so several assertions were looser than the
// real types. The exact assertions below pin what the 3.0.5 engine actually
// generates: response fields are `undefined` when unset (never `null` —
// honest `| null` response types arrived in genql v4), and `__typename` is a
// literal type, not `string`.
const SCHEMA_PATH = join(
dirname(fileURLToPath(import.meta.url)),
'fixture',
'schema.graphql',
);
const executableSchema: GraphQLSchema = buildSchema(
readFileSync(SCHEMA_PATH, 'utf-8'),
);
const johnUser = {
name: 'John',
};
// The Query resolvers from upstream's makeServer(), as graphql-js rootValue
// field functions (they receive the field args as first parameter).
const queryRootValue = {
user: () => johnUser,
throwsError: () => {
throw new Error('x');
},
optionalArgs: () => ({ createdAt: 'now' }),
unionThatImplementsInterface: ({ typename = '' }: { typename?: string }) => ({
message: 'A message',
ownProp1: 'Own prop 1',
ownProp2: 'Own prop 2',
__typename: typename || 'ClientErrorNameInvalid',
}),
someScalarValue: () => 'someScalarValue',
repository: () => ({ createdAt: 'now' }),
account: () => ({ __typename: 'User', ...johnUser }),
coordinates: () => ({ __typename: 'Bank', x: '1', y: '2', address: '3' }),
};
const executeOperation = async (
operation: GraphqlOperation,
): Promise<ExecutionResult> =>
await graphql({
schema: executableSchema,
source: operation.query,
variableValues: operation.variables,
rootValue: queryRootValue,
});
const executableFetch = vi.fn(
async (_url: RequestInfo | URL, requestInit?: RequestInit) => {
const body = JSON.parse(String(requestInit?.body)) as
| GraphqlOperation
| GraphqlOperation[];
const payload = Array.isArray(body)
? await Promise.all(body.map(executeOperation))
: await executeOperation(body);
return new Response(JSON.stringify(payload), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
},
) as unknown as typeof globalThis.fetch;
const URL_PLACEHOLDER = 'http://executable-schema.test/graphql';
describe('execute queries', () => {
const client = createClient({
url: URL_PLACEHOLDER,
headers: () => ({ Auth: 'xxx' }),
fetch: executableFetch,
});
it('first query', async () => {
const res = await client.query({
repository: {
__args: {
name: 'genql',
},
createdAt: true,
},
optionalArgs: {
createdAt: true,
},
});
expect(res.repository.createdAt).toBeTruthy();
expect(res.optionalArgs.createdAt).toBeTruthy();
});
it('simple', async () => {
const res = await client.query({
user: {
name: true,
},
});
expect(res.user).toEqual(johnUser);
});
it('__typename is not optional', async () => {
const res = await client.query({
user: {
name: true,
__typename: true,
},
});
expectTypeOf(res.user!.__typename).toEqualTypeOf<'User'>();
expect(res.user?.__typename).toBe('User');
});
it('scalar value with argument', async () => {
const withoutArgs = await client.query({
someScalarValue: true,
});
expect(withoutArgs.someScalarValue?.toLocaleLowerCase).toBeTruthy();
const withArgs = await client.query({
someScalarValue: { __args: { x: 1 } },
});
expect(withArgs.someScalarValue?.toLocaleLowerCase).toBeTruthy();
});
it('falsy values are not fetched', async () => {
const res = await client.query({
coordinates: {
x: false,
y: true,
},
});
expect(res.coordinates?.x).toBeUndefined();
expect(res.coordinates?.y).toBeDefined();
});
it('required field and nested fields', async () => {
const res = await client.query({
repository: {
__args: {
name: 'genql',
owner: 'remorses',
},
...everything,
forks: {
__args: { filter: 'test' },
edges: { node: { name: true, number: true } },
},
},
});
// @ts-expect-error top level fields are filtered based on the query
void res?.account;
// no optional chaining because repository is non null
expectTypeOf(res.repository.createdAt).toEqualTypeOf<string>();
expectTypeOf(res.repository.__typename).toEqualTypeOf<'Repository'>();
expectTypeOf(
res.repository?.forks?.edges?.map((edge) => edge?.node?.name),
).toEqualTypeOf<(string | undefined)[] | undefined>();
expectTypeOf(
res.repository?.forks?.edges?.map((edge) => edge?.node?.number),
).toEqualTypeOf<(number | undefined)[] | undefined>();
expect(res.repository.createdAt).toBeTruthy();
});
it('union types only 1 on_ normal syntax', async () => {
const { account } = await client.query({
account: {
__typename: 1,
on_User: {
name: 1,
},
},
});
// @ts-expect-error on_User should be removed from the response type
void account?.on_User;
expect(account?.__typename).toBeTruthy();
expectTypeOf(account).toExtend<Account | undefined>();
expectTypeOf(account?.__typename).toEqualTypeOf<
'User' | 'Guest' | undefined
>();
});
it('ability to query interfaces that a union implements', async () => {
const { unionThatImplementsInterface } = await client.query({
unionThatImplementsInterface: {
__typename: 1,
on_ClientErrorNameInvalid: {
message: 1,
ownProp2: 1,
},
on_ClientError: {
message: 1,
},
on_ClientErrorWithoutInterface: {
ownProp3: 1,
},
},
});
if (unionThatImplementsInterface?.__typename === 'ClientErrorNameInvalid') {
expect(unionThatImplementsInterface?.ownProp2).toBeTruthy();
}
if (
unionThatImplementsInterface?.__typename === 'ClientErrorWithoutInterface'
) {
expect(unionThatImplementsInterface?.ownProp3).toBeTruthy();
}
expect(unionThatImplementsInterface?.__typename).toBe(
'ClientErrorNameInvalid',
);
});
it('interface types normal syntax', async () => {
const res = await client.query({
coordinates: {
x: 1,
__typename: 1,
on_Bank: {
address: 1,
},
},
});
const coordinates = res.coordinates;
expectTypeOf(coordinates?.x).toEqualTypeOf<string | undefined>();
if (coordinates && 'address' in coordinates) {
expectTypeOf(coordinates?.address).toEqualTypeOf<string | undefined>();
expect(coordinates?.address).toBeTruthy();
}
// common types are accessible without guards
expect(coordinates?.x).toBeTruthy();
expect(coordinates?.__typename).toBeTruthy();
});
it('multiple interfaces types normal syntax', async () => {
const { coordinates } = await client.query({
coordinates: {
__typename: 1,
on_Bank: {
address: 1,
x: 1,
},
on_House: {
y: 1,
x: 1,
owner: {
name: 1,
},
},
},
});
expectTypeOf(coordinates?.x).toEqualTypeOf<string | undefined>();
expectTypeOf(coordinates).toExtend<Point | undefined>();
expectTypeOf(coordinates?.__typename).toEqualTypeOf<
'Bank' | 'House' | undefined
>();
expect(coordinates?.x).toBeTruthy();
expect(coordinates?.__typename).toBeTruthy();
if (coordinates && 'address' in coordinates) {
void coordinates?.address;
void coordinates?.x;
} else if (isHouse(coordinates)) {
void coordinates?.owner;
void coordinates?.x;
void coordinates?.y;
}
});
it('errors', async () => {
const err = await client
.query({
throwsError: true,
})
.catch((error) => error);
expect(err).toBeInstanceOf(Error);
expect(err).toBeInstanceOf(GenqlError);
expect((err as GenqlError).errors.length).toBeTruthy();
});
it('errors are thrown with batching', async () => {
const batchingClient = createClient({
url: URL_PLACEHOLDER,
fetch: executableFetch,
batch: {
batchInterval: 100,
},
});
const [err1, err2] = await Promise.all([
batchingClient
.query({
throwsError: true,
})
.catch((error) => error),
batchingClient
.query({
throwsError: true,
})
.catch((error) => error),
]);
expect(err1).toBeInstanceOf(GenqlError);
expect(err2).toBeInstanceOf(GenqlError);
expect((err1 as GenqlError).errors.length).toBeTruthy();
expect((err2 as GenqlError).errors.length).toBeTruthy();
});
it('1 error and 1 result with batching', async () => {
const batchingClient = createClient({
url: URL_PLACEHOLDER,
fetch: executableFetch,
batch: {
batchInterval: 100,
},
});
const [err1, res] = await Promise.all([
batchingClient
.query({
throwsError: true,
})
.catch((error) => error),
batchingClient.query({
user: {
name: true,
},
}),
]);
expect(err1).toBeInstanceOf(GenqlError);
expect((err1 as GenqlError).errors.length).toBeTruthy();
expect(res.user).toEqual(johnUser);
});
it('batches requests', async () => {
let batchedQueryLength = -1;
let requestsCount = 0;
const batchingClient = createClient({
batch: true,
fetcher: async (body) => {
requestsCount += 1;
batchedQueryLength = Array.isArray(body) ? body.length : -1;
const payload = Array.isArray(body)
? await Promise.all(body.map(executeOperation))
: await executeOperation(body);
// the real transport JSON-serializes; mirror it so GraphQLError
// instances become the plain objects a fetcher returns
return JSON.parse(JSON.stringify(payload));
},
});
const res = await Promise.all([
batchingClient.query({
coordinates: {
__typename: 1,
x: 1,
},
}),
batchingClient.query({
coordinates: {
__typename: 1,
y: 1,
},
}),
]);
expect(res.length).toBe(2);
expect(batchedQueryLength).toBe(2);
expect(requestsCount).toBe(1);
});
it('headers function gets called every time', async () => {
let headersCalledNTimes = 0;
const countingClient = createClient({
url: URL_PLACEHOLDER,
fetch: executableFetch,
headers: () => {
headersCalledNTimes++;
return {};
},
});
await countingClient.query({
coordinates: {
__typename: 1,
x: 1,
},
});
await countingClient.query({
coordinates: {
__typename: 1,
y: 1,
},
});
expect(headersCalledNTimes).toBe(2);
});
it('async headers function gets called every time', async () => {
let headersCalledNTimes = 0;
const countingClient = createClient({
url: URL_PLACEHOLDER,
fetch: executableFetch,
headers: async () => {
headersCalledNTimes++;
return {};
},
});
await countingClient.query({
coordinates: {
__typename: 1,
x: 1,
},
});
await countingClient.query({
coordinates: {
__typename: 1,
y: 1,
},
});
expect(headersCalledNTimes).toBe(2);
});
});
// upstream asserted `DeepPartial<User>` assignability for the mock; keep the
// equivalent compile-time check without the tsdef dependency.
expectTypeOf(johnUser).toExtend<Partial<User>>();

View File

@@ -0,0 +1,65 @@
// @ts-nocheck
import type {
QueryGenqlSelection,
Query,
SubscriptionGenqlSelection,
Subscription,
} from './schema'
import {
linkTypeMap,
createClient as createClientOriginal,
generateGraphqlOperation,
type FieldsSelection,
type GraphqlOperation,
type ClientOptions,
GenqlError,
} from './runtime'
export type { FieldsSelection } from './runtime'
export { GenqlError }
import types from './types'
export * from './schema'
const typeMap = linkTypeMap(types as any)
export interface Client {
query<R extends QueryGenqlSelection>(
request: R & { __name?: string },
): Promise<FieldsSelection<Query, R>>
}
export const createClient = function (options?: ClientOptions): Client {
return createClientOriginal({
url: undefined,
...options,
queryRoot: typeMap.Query!,
mutationRoot: typeMap.Mutation!,
subscriptionRoot: typeMap.Subscription!,
}) as any
}
export const everything = {
__scalar: true,
}
export type QueryResult<fields extends QueryGenqlSelection> = FieldsSelection<
Query,
fields
>
export const generateQueryOp: (
fields: QueryGenqlSelection & { __name?: string },
) => GraphqlOperation = function (fields) {
return generateGraphqlOperation('query', typeMap.Query!, fields as any)
}
export type SubscriptionResult<fields extends SubscriptionGenqlSelection> =
FieldsSelection<Subscription, fields>
export const generateSubscriptionOp: (
fields: SubscriptionGenqlSelection & { __name?: string },
) => GraphqlOperation = function (fields) {
return generateGraphqlOperation(
'subscription',
typeMap.Subscription!,
fields as any,
)
}

View File

@@ -0,0 +1,271 @@
// @ts-nocheck
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
}
}

View File

@@ -0,0 +1,68 @@
// @ts-nocheck
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
}

View File

@@ -0,0 +1,29 @@
// @ts-nocheck
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>
}

View File

@@ -0,0 +1,98 @@
// @ts-nocheck
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),
)
}
}

View File

@@ -0,0 +1,225 @@
// @ts-nocheck
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
}

View File

@@ -0,0 +1,13 @@
// @ts-nocheck
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,
}

View File

@@ -0,0 +1,139 @@
// @ts-nocheck
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
}

View File

@@ -0,0 +1,98 @@
// @ts-nocheck
//////////////////////////////////////////////////
// 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

View File

@@ -0,0 +1,69 @@
// @ts-nocheck
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
}

View File

@@ -0,0 +1,110 @@
enum SomeEnum {
X
Y
Z
}
enum SomeEnum2 {
hello
world
}
type Query {
"""Some description"""
repository(name: String!, owner: String): Repository!
optionalArgs(name: String, owner: String): Repository!
user: User
someScalarValue(x: Float): String
recursiveType(requiredVal: [String!]): [RecursiveType]
throwsError: String!
account: Account
coordinates: Point
unionThatImplementsInterface(typename: String): GenericError
InterfaceNotImplemented: InterfaceNotImplemented
}
type RecursiveType {
value: String
recurse(arg: Int): RecursiveType
}
type Repository {
createdAt: String!
forks(filter: String): ForkConnection
}
type ForkConnection {
edges: [ForkEdge]
}
type ForkEdge {
cursor: String
node: Fork
}
type Fork {
name: String
number: Int
}
type User {
"""Some description"""
name: String
common: Int
}
type Subscription {
user: User
}
union Account = User | Guest
type Guest {
anonymous: Boolean
common: Int
}
type House implements Point {
owner: User
x: String
y: String
}
type Bank implements Point {
address: String
x: String
y: String
}
interface Point {
x: String
y: String
}
interface ClientError {
message: String!
}
type ClientErrorNameAlreadyTaken implements ClientError {
message: String!
ownProp1: String
}
type ClientErrorNameInvalid implements ClientError {
message: String!
ownProp2: String
}
type ClientErrorWithoutInterface {
ownProp3: String
}
union GenericError = ClientErrorNameAlreadyTaken | ClientErrorNameInvalid | ClientErrorWithoutInterface
interface InterfaceNotImplemented {
id: ID!
title: String
url: String
permalink: String
entry_id: ID
}

View File

@@ -0,0 +1,433 @@
// @ts-nocheck
export type Scalars = {
String: string,
Float: number,
Int: number,
Boolean: boolean,
ID: string,
}
export type SomeEnum = 'X' | 'Y' | 'Z'
export type SomeEnum2 = 'hello' | 'world'
export interface Query {
/** Some description */
repository: Repository
optionalArgs: Repository
user?: User
someScalarValue?: Scalars['String']
recursiveType?: (RecursiveType | undefined)[]
throwsError: Scalars['String']
account?: Account
coordinates?: Point
unionThatImplementsInterface?: GenericError
InterfaceNotImplemented?: InterfaceNotImplemented
__typename: 'Query'
}
export interface RecursiveType {
value?: Scalars['String']
recurse?: RecursiveType
__typename: 'RecursiveType'
}
export interface Repository {
createdAt: Scalars['String']
forks?: ForkConnection
__typename: 'Repository'
}
export interface ForkConnection {
edges?: (ForkEdge | undefined)[]
__typename: 'ForkConnection'
}
export interface ForkEdge {
cursor?: Scalars['String']
node?: Fork
__typename: 'ForkEdge'
}
export interface Fork {
name?: Scalars['String']
number?: Scalars['Int']
__typename: 'Fork'
}
export interface User {
/** Some description */
name?: Scalars['String']
common?: Scalars['Int']
__typename: 'User'
}
export interface Subscription {
user?: User
__typename: 'Subscription'
}
export type Account = (User | Guest) & { __isUnion?: true }
export interface Guest {
anonymous?: Scalars['Boolean']
common?: Scalars['Int']
__typename: 'Guest'
}
export interface House {
owner?: User
x?: Scalars['String']
y?: Scalars['String']
__typename: 'House'
}
export interface Bank {
address?: Scalars['String']
x?: Scalars['String']
y?: Scalars['String']
__typename: 'Bank'
}
export type Point = (House | Bank) & { __isUnion?: true }
export type ClientError = (ClientErrorNameAlreadyTaken | ClientErrorNameInvalid) & { __isUnion?: true }
export interface ClientErrorNameAlreadyTaken {
message: Scalars['String']
ownProp1?: Scalars['String']
__typename: 'ClientErrorNameAlreadyTaken'
}
export interface ClientErrorNameInvalid {
message: Scalars['String']
ownProp2?: Scalars['String']
__typename: 'ClientErrorNameInvalid'
}
export interface ClientErrorWithoutInterface {
ownProp3?: Scalars['String']
__typename: 'ClientErrorWithoutInterface'
}
export type GenericError = (ClientErrorNameAlreadyTaken | ClientErrorNameInvalid | ClientErrorWithoutInterface) & { __isUnion?: true }
export interface InterfaceNotImplemented {
id: Scalars['ID']
title?: Scalars['String']
url?: Scalars['String']
permalink?: Scalars['String']
entry_id?: Scalars['ID']
__typename: string
}
export interface QueryGenqlSelection{
/** Some description */
repository?: (RepositoryGenqlSelection & { __args: {name: Scalars['String'], owner?: (Scalars['String'] | null)} })
optionalArgs?: (RepositoryGenqlSelection & { __args?: {name?: (Scalars['String'] | null), owner?: (Scalars['String'] | null)} })
user?: UserGenqlSelection
someScalarValue?: { __args: {x?: (Scalars['Float'] | null)} } | boolean | number
recursiveType?: (RecursiveTypeGenqlSelection & { __args?: {requiredVal?: (Scalars['String'][] | null)} })
throwsError?: boolean | number
account?: AccountGenqlSelection
coordinates?: PointGenqlSelection
unionThatImplementsInterface?: (GenericErrorGenqlSelection & { __args?: {typename?: (Scalars['String'] | null)} })
InterfaceNotImplemented?: InterfaceNotImplementedGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface RecursiveTypeGenqlSelection{
value?: boolean | number
recurse?: (RecursiveTypeGenqlSelection & { __args?: {arg?: (Scalars['Int'] | null)} })
__typename?: boolean | number
__scalar?: boolean | number
}
export interface RepositoryGenqlSelection{
createdAt?: boolean | number
forks?: (ForkConnectionGenqlSelection & { __args?: {filter?: (Scalars['String'] | null)} })
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ForkConnectionGenqlSelection{
edges?: ForkEdgeGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ForkEdgeGenqlSelection{
cursor?: boolean | number
node?: ForkGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ForkGenqlSelection{
name?: boolean | number
number?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface UserGenqlSelection{
/** Some description */
name?: boolean | number
common?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface SubscriptionGenqlSelection{
user?: UserGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface AccountGenqlSelection{
on_User?:UserGenqlSelection,
on_Guest?:GuestGenqlSelection,
__typename?: boolean | number
}
export interface GuestGenqlSelection{
anonymous?: boolean | number
common?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface HouseGenqlSelection{
owner?: UserGenqlSelection
x?: boolean | number
y?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface BankGenqlSelection{
address?: boolean | number
x?: boolean | number
y?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface PointGenqlSelection{
x?: boolean | number
y?: boolean | number
on_House?: HouseGenqlSelection
on_Bank?: BankGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ClientErrorGenqlSelection{
message?: boolean | number
on_ClientErrorNameAlreadyTaken?: ClientErrorNameAlreadyTakenGenqlSelection
on_ClientErrorNameInvalid?: ClientErrorNameInvalidGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ClientErrorNameAlreadyTakenGenqlSelection{
message?: boolean | number
ownProp1?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ClientErrorNameInvalidGenqlSelection{
message?: boolean | number
ownProp2?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ClientErrorWithoutInterfaceGenqlSelection{
ownProp3?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface GenericErrorGenqlSelection{
on_ClientErrorNameAlreadyTaken?:ClientErrorNameAlreadyTakenGenqlSelection,
on_ClientErrorNameInvalid?:ClientErrorNameInvalidGenqlSelection,
on_ClientErrorWithoutInterface?:ClientErrorWithoutInterfaceGenqlSelection,
on_ClientError?: ClientErrorGenqlSelection,
__typename?: boolean | number
}
export interface InterfaceNotImplementedGenqlSelection{
id?: boolean | number
title?: boolean | number
url?: boolean | number
permalink?: boolean | number
entry_id?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
const Query_possibleTypes: string[] = ['Query']
export const isQuery = (obj?: { __typename?: any } | null): obj is Query => {
if (!obj?.__typename) throw new Error('__typename is missing in "isQuery"')
return Query_possibleTypes.includes(obj.__typename)
}
const RecursiveType_possibleTypes: string[] = ['RecursiveType']
export const isRecursiveType = (obj?: { __typename?: any } | null): obj is RecursiveType => {
if (!obj?.__typename) throw new Error('__typename is missing in "isRecursiveType"')
return RecursiveType_possibleTypes.includes(obj.__typename)
}
const Repository_possibleTypes: string[] = ['Repository']
export const isRepository = (obj?: { __typename?: any } | null): obj is Repository => {
if (!obj?.__typename) throw new Error('__typename is missing in "isRepository"')
return Repository_possibleTypes.includes(obj.__typename)
}
const ForkConnection_possibleTypes: string[] = ['ForkConnection']
export const isForkConnection = (obj?: { __typename?: any } | null): obj is ForkConnection => {
if (!obj?.__typename) throw new Error('__typename is missing in "isForkConnection"')
return ForkConnection_possibleTypes.includes(obj.__typename)
}
const ForkEdge_possibleTypes: string[] = ['ForkEdge']
export const isForkEdge = (obj?: { __typename?: any } | null): obj is ForkEdge => {
if (!obj?.__typename) throw new Error('__typename is missing in "isForkEdge"')
return ForkEdge_possibleTypes.includes(obj.__typename)
}
const Fork_possibleTypes: string[] = ['Fork']
export const isFork = (obj?: { __typename?: any } | null): obj is Fork => {
if (!obj?.__typename) throw new Error('__typename is missing in "isFork"')
return Fork_possibleTypes.includes(obj.__typename)
}
const User_possibleTypes: string[] = ['User']
export const isUser = (obj?: { __typename?: any } | null): obj is User => {
if (!obj?.__typename) throw new Error('__typename is missing in "isUser"')
return User_possibleTypes.includes(obj.__typename)
}
const Subscription_possibleTypes: string[] = ['Subscription']
export const isSubscription = (obj?: { __typename?: any } | null): obj is Subscription => {
if (!obj?.__typename) throw new Error('__typename is missing in "isSubscription"')
return Subscription_possibleTypes.includes(obj.__typename)
}
const Account_possibleTypes: string[] = ['User','Guest']
export const isAccount = (obj?: { __typename?: any } | null): obj is Account => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAccount"')
return Account_possibleTypes.includes(obj.__typename)
}
const Guest_possibleTypes: string[] = ['Guest']
export const isGuest = (obj?: { __typename?: any } | null): obj is Guest => {
if (!obj?.__typename) throw new Error('__typename is missing in "isGuest"')
return Guest_possibleTypes.includes(obj.__typename)
}
const House_possibleTypes: string[] = ['House']
export const isHouse = (obj?: { __typename?: any } | null): obj is House => {
if (!obj?.__typename) throw new Error('__typename is missing in "isHouse"')
return House_possibleTypes.includes(obj.__typename)
}
const Bank_possibleTypes: string[] = ['Bank']
export const isBank = (obj?: { __typename?: any } | null): obj is Bank => {
if (!obj?.__typename) throw new Error('__typename is missing in "isBank"')
return Bank_possibleTypes.includes(obj.__typename)
}
const Point_possibleTypes: string[] = ['House','Bank']
export const isPoint = (obj?: { __typename?: any } | null): obj is Point => {
if (!obj?.__typename) throw new Error('__typename is missing in "isPoint"')
return Point_possibleTypes.includes(obj.__typename)
}
const ClientError_possibleTypes: string[] = ['ClientErrorNameAlreadyTaken','ClientErrorNameInvalid']
export const isClientError = (obj?: { __typename?: any } | null): obj is ClientError => {
if (!obj?.__typename) throw new Error('__typename is missing in "isClientError"')
return ClientError_possibleTypes.includes(obj.__typename)
}
const ClientErrorNameAlreadyTaken_possibleTypes: string[] = ['ClientErrorNameAlreadyTaken']
export const isClientErrorNameAlreadyTaken = (obj?: { __typename?: any } | null): obj is ClientErrorNameAlreadyTaken => {
if (!obj?.__typename) throw new Error('__typename is missing in "isClientErrorNameAlreadyTaken"')
return ClientErrorNameAlreadyTaken_possibleTypes.includes(obj.__typename)
}
const ClientErrorNameInvalid_possibleTypes: string[] = ['ClientErrorNameInvalid']
export const isClientErrorNameInvalid = (obj?: { __typename?: any } | null): obj is ClientErrorNameInvalid => {
if (!obj?.__typename) throw new Error('__typename is missing in "isClientErrorNameInvalid"')
return ClientErrorNameInvalid_possibleTypes.includes(obj.__typename)
}
const ClientErrorWithoutInterface_possibleTypes: string[] = ['ClientErrorWithoutInterface']
export const isClientErrorWithoutInterface = (obj?: { __typename?: any } | null): obj is ClientErrorWithoutInterface => {
if (!obj?.__typename) throw new Error('__typename is missing in "isClientErrorWithoutInterface"')
return ClientErrorWithoutInterface_possibleTypes.includes(obj.__typename)
}
const GenericError_possibleTypes: string[] = ['ClientErrorNameAlreadyTaken','ClientErrorNameInvalid','ClientErrorWithoutInterface']
export const isGenericError = (obj?: { __typename?: any } | null): obj is GenericError => {
if (!obj?.__typename) throw new Error('__typename is missing in "isGenericError"')
return GenericError_possibleTypes.includes(obj.__typename)
}
const InterfaceNotImplemented_possibleTypes: string[] = []
export const isInterfaceNotImplemented = (obj?: { __typename?: any } | null): obj is InterfaceNotImplemented => {
if (!obj?.__typename) throw new Error('__typename is missing in "isInterfaceNotImplemented"')
return InterfaceNotImplemented_possibleTypes.includes(obj.__typename)
}
export const enumSomeEnum = {
X: 'X' as const,
Y: 'Y' as const,
Z: 'Z' as const
}
export const enumSomeEnum2 = {
hello: 'hello' as const,
world: 'world' as const
}

View File

@@ -0,0 +1,317 @@
export default {
"scalars": [
0,
1,
3,
4,
6,
15,
25
],
"types": {
"SomeEnum": {},
"SomeEnum2": {},
"Query": {
"repository": [
7,
{
"name": [
3,
"String!"
],
"owner": [
3
]
}
],
"optionalArgs": [
7,
{
"name": [
3
],
"owner": [
3
]
}
],
"user": [
11
],
"someScalarValue": [
3,
{
"x": [
4
]
}
],
"recursiveType": [
5,
{
"requiredVal": [
3,
"[String!]"
]
}
],
"throwsError": [
3
],
"account": [
13
],
"coordinates": [
18
],
"unionThatImplementsInterface": [
23,
{
"typename": [
3
]
}
],
"InterfaceNotImplemented": [
24
],
"__typename": [
3
]
},
"String": {},
"Float": {},
"RecursiveType": {
"value": [
3
],
"recurse": [
5,
{
"arg": [
6
]
}
],
"__typename": [
3
]
},
"Int": {},
"Repository": {
"createdAt": [
3
],
"forks": [
8,
{
"filter": [
3
]
}
],
"__typename": [
3
]
},
"ForkConnection": {
"edges": [
9
],
"__typename": [
3
]
},
"ForkEdge": {
"cursor": [
3
],
"node": [
10
],
"__typename": [
3
]
},
"Fork": {
"name": [
3
],
"number": [
6
],
"__typename": [
3
]
},
"User": {
"name": [
3
],
"common": [
6
],
"__typename": [
3
]
},
"Subscription": {
"user": [
11
],
"__typename": [
3
]
},
"Account": {
"on_User": [
11
],
"on_Guest": [
14
],
"__typename": [
3
]
},
"Guest": {
"anonymous": [
15
],
"common": [
6
],
"__typename": [
3
]
},
"Boolean": {},
"House": {
"owner": [
11
],
"x": [
3
],
"y": [
3
],
"__typename": [
3
]
},
"Bank": {
"address": [
3
],
"x": [
3
],
"y": [
3
],
"__typename": [
3
]
},
"Point": {
"x": [
3
],
"y": [
3
],
"on_House": [
16
],
"on_Bank": [
17
],
"__typename": [
3
]
},
"ClientError": {
"message": [
3
],
"on_ClientErrorNameAlreadyTaken": [
20
],
"on_ClientErrorNameInvalid": [
21
],
"__typename": [
3
]
},
"ClientErrorNameAlreadyTaken": {
"message": [
3
],
"ownProp1": [
3
],
"__typename": [
3
]
},
"ClientErrorNameInvalid": {
"message": [
3
],
"ownProp2": [
3
],
"__typename": [
3
]
},
"ClientErrorWithoutInterface": {
"ownProp3": [
3
],
"__typename": [
3
]
},
"GenericError": {
"on_ClientErrorNameAlreadyTaken": [
20
],
"on_ClientErrorNameInvalid": [
21
],
"on_ClientErrorWithoutInterface": [
22
],
"on_ClientError": [
19
],
"__typename": [
3
]
},
"InterfaceNotImplemented": {
"id": [
25
],
"title": [
3
],
"url": [
3
],
"permalink": [
3
],
"entry_id": [
25
],
"__typename": [
3
]
},
"ID": {}
}
}

View File

@@ -0,0 +1,134 @@
enum SomeEnum {
X
Y
Z
}
enum SomeEnum2 {
hello
world
}
# Some comment
type Query {
"""
Some description
"""
repository(name: String!, owner: String): Repository!
optionalArgs(name: String, owner: String): Repository!
user: User
someScalarValue(x: Float): String
recursiveType(requiredVal: [String!]): [RecursiveType]
throwsError: String!
}
type RecursiveType {
value: String
recurse(arg: Int): RecursiveType
}
type Repository {
createdAt: String!
forks(filter: String): ForkConnection
}
type ForkConnection {
edges: [ForkEdge]
}
type ForkEdge {
cursor: String
node: Fork
}
type Fork {
name: String
number: Int
}
type User {
"""
Some description
"""
name: String
common: Int
}
# ---
type Subscription {
user: User
}
##################
extend type Query {
account: Account
}
union Account = User | Guest
type Guest {
anonymous: Boolean
common: Int
}
##################
extend type Query {
coordinates: Point
}
type House implements Point {
owner: User
x: String
y: String
}
type Bank implements Point {
address: String
x: String
y: String
}
interface Point {
x: String
y: String
}
# Ability to query interfaces that a union implements https://github.com/remorses/genql/issues/44
interface ClientError {
message: String!
}
type ClientErrorNameAlreadyTaken implements ClientError {
message: String!
ownProp1: String
}
type ClientErrorNameInvalid implements ClientError {
message: String!
ownProp2: String
}
type ClientErrorWithoutInterface {
ownProp3: String
}
union GenericError =
| ClientErrorNameAlreadyTaken
| ClientErrorNameInvalid
| ClientErrorWithoutInterface
extend type Query {
unionThatImplementsInterface(typename: String): GenericError
InterfaceNotImplemented: InterfaceNotImplemented
}
interface InterfaceNotImplemented {
id: ID!
title: String
url: String
permalink: String
entry_id: ID
}

View File

@@ -0,0 +1,175 @@
import { describe, expect, expectTypeOf, it } from 'vitest';
import { prettify } from '../../genql/helpers/prettify';
import {
enumSomeEnum,
everything,
generateQueryOp,
generateSubscriptionOp,
} from './fixture/generated';
// Port of remorses/genql@v3.0.5 integration-tests/tests/simple.ts ("generate
// queries") onto vitest: mocha `describe/it` and snap-shot-it snapshots become
// vitest snapshots, tsd's `expectType` becomes `expectTypeOf`. The selections
// and test names are upstream's, unchanged.
describe('generate queries', () => {
it('enum string is present', () => {
expectTypeOf(enumSomeEnum.X).toEqualTypeOf<'X'>();
expect(enumSomeEnum.X).toBe('X');
expect(enumSomeEnum.Y).toBe('Y');
});
it('query', async () => {
const { query } = generateQueryOp({
repository: {
__args: {
name: 'repo',
owner: 'owner',
},
createdAt: true,
forks: {
edges: {
cursor: true,
node: {
...everything,
},
},
},
},
});
expect(await prettify(query, 'graphql')).toMatchSnapshot();
});
it('optional arg', async () => {
const { query } = generateQueryOp({
optionalArgs: {
createdAt: true,
forks: {
edges: {
cursor: true,
node: {
...everything,
},
},
},
},
});
expect(await prettify(query, 'graphql')).toMatchSnapshot();
});
it('recursive type', async () => {
const { query } = generateQueryOp({
recursiveType: {
value: 1,
recurse: {
...everything,
recurse: {
value: 1,
recurse: {
...everything,
recurse: {
...everything,
},
},
},
},
},
});
expect(await prettify(query, 'graphql')).toMatchSnapshot();
});
it('recursive type with args', async () => {
const { query } = generateQueryOp({
recursiveType: {
__args: { requiredVal: ['ciao'] },
value: 1,
recurse: {
__args: {
arg: 1,
},
...everything,
recurse: {
__args: {
arg: 1,
},
value: 1,
recurse: {
__args: {
arg: 1,
},
...everything,
recurse: {
...everything,
},
},
},
},
},
});
expect(await prettify(query, 'graphql')).toMatchSnapshot();
});
it('use __name operation name', async () => {
const NAME = 'SomeName';
const { query } = generateSubscriptionOp({
__name: NAME,
user: {
__scalar: true,
},
});
expect(await prettify(query, 'graphql')).toMatchSnapshot();
});
it('subscriptions', async () => {
const { query } = generateSubscriptionOp({
user: {
__scalar: true,
},
});
expect(await prettify(query, 'graphql')).toMatchSnapshot();
});
it('many', async () => {
const { query } = generateQueryOp({
repository: {
__args: {
name: 'repo',
owner: 'owner',
},
createdAt: true,
forks: {
edges: {
cursor: true,
node: {
...everything,
},
},
},
},
user: {
...everything,
},
});
expect(await prettify(query, 'graphql')).toMatchSnapshot();
});
it('do not fetch falsy fields', async () => {
const { query } = generateSubscriptionOp({
user: {
common: false,
name: true,
},
});
expect(await prettify(query, 'graphql')).toMatchSnapshot();
});
it('do not fetch falsy fields with __scalar', async () => {
const { query } = generateSubscriptionOp({
user: {
common: false,
__scalar: true,
},
});
expect(await prettify(query, 'graphql')).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,83 @@
import { readFileSync } from 'node:fs';
import { cp, mkdtemp, readdir, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { generate } from '../../genql';
// The checked-in client under fixture/generated is the vendored genql engine's
// output for upstream's own integration-test schema (fixture/schema.graphql,
// verbatim from remorses/genql@v3.0.5 integration-tests/). Checking it in lets
// the ported upstream tests import it statically, so their tsd-style type
// assertions are verified by the package typecheck. This test regenerates the
// client and compares byte-for-byte so the fixture can never drift from what
// the engine actually produces.
//
// To refresh after an intentional engine change:
// UPDATE_GENQL_UPSTREAM_FIXTURE=1 npx vitest run src/generate/__tests__/upstream-3.0.5/generated-fixture-drift.test.ts
const FIXTURE_DIR = join(
dirname(fileURLToPath(import.meta.url)),
'fixture',
'generated',
);
const SCHEMA_PATH = join(
dirname(fileURLToPath(import.meta.url)),
'fixture',
'schema.graphql',
);
const listFilesRecursively = async (root: string): Promise<string[]> => {
const entries = await readdir(root, { recursive: true, withFileTypes: true });
return entries
.filter((entry) => entry.isFile())
.map((entry) => relative(root, join(entry.parentPath, entry.name)))
.sort();
};
describe('upstream fixture client matches the engine output', () => {
let temporaryDir: string;
let regeneratedDir: string;
beforeAll(async () => {
temporaryDir = await mkdtemp(join(tmpdir(), 'twenty-genql-fixture-'));
regeneratedDir = join(temporaryDir, 'generated');
await generate({
schema: readFileSync(SCHEMA_PATH, 'utf-8'),
output: regeneratedDir,
});
if (
['1', 'true'].includes(process.env.UPDATE_GENQL_UPSTREAM_FIXTURE ?? '')
) {
await rm(FIXTURE_DIR, { recursive: true, force: true });
await cp(regeneratedDir, FIXTURE_DIR, { recursive: true });
}
}, 60000);
afterAll(async () => {
if (temporaryDir) {
await rm(temporaryDir, { recursive: true, force: true });
}
});
it('produces the same file list', async () => {
expect(await listFilesRecursively(regeneratedDir)).toEqual(
await listFilesRecursively(FIXTURE_DIR),
);
});
it('produces byte-identical file contents', async () => {
for (const file of await listFilesRecursively(FIXTURE_DIR)) {
const regenerated = await readFile(join(regeneratedDir, file), 'utf-8');
const checkedIn = await readFile(join(FIXTURE_DIR, file), 'utf-8');
expect(regenerated, `fixture/generated/${file} is stale`).toBe(checkedIn);
}
});
});

View File

@@ -0,0 +1,23 @@
import { describe, expect, test } from 'vitest';
import { prettify } from '../../genql/helpers/prettify';
import { RenderContext } from '../../genql/render/common/RenderContext';
// Port of remorses/genql@v3.0.5
// cli/src/render/common/__tests__/RenderContext.test.ts; `toCode` and
// `prettify` are awaited since the prettier 3 migration made them async.
describe('RenderContext', () => {
test('prettify', async () => {
const renderContext = new RenderContext();
renderContext.addCodeBlock('interface A{}');
expect(await renderContext.toCode('typescript', true)).toBe(
await prettify('interface A{}', 'typescript'),
);
});
test('raw', async () => {
const renderContext = new RenderContext();
renderContext.addCodeBlock('raw string');
expect(await renderContext.toCode()).toBe('raw string');
});
});

View File

@@ -0,0 +1,46 @@
import { expect, test } from 'vitest';
import { prettify } from '../../genql/helpers/prettify';
import { renderSchema } from '../../genql/render/schema/renderSchema';
import { schemaRenderTest } from './render-test-helpers';
// Port of remorses/genql@v3.0.5 cli/src/render/schema/renderSchema.test.ts;
// `prettify` is awaited since the prettier 3 migration made it async.
test('renderSchema', async () => {
expect(
await schemaRenderTest(
/* GraphQL */ `
type A {
some: String
}
type B {
some: String @deprecated
}
type Query {
_: Boolean
}
`,
renderSchema,
'graphql',
),
).toBe(
await prettify(
/* GraphQL */ `
type A {
some: String
}
type B {
some: String @deprecated
}
type Query {
_: Boolean
}
`,
'graphql',
),
);
});

View File

@@ -0,0 +1,60 @@
import {
buildClientSchema,
buildSchema,
getIntrospectionQuery,
graphql,
type GraphQLNamedType,
type GraphQLSchema,
type IntrospectionQuery,
} from 'graphql';
import { type BuiltInParserName } from 'prettier';
import { RenderContext } from '../../genql/render/common/RenderContext';
// Port of remorses/genql@v3.0.5 cli/src/testHelpers/render.ts (minus the
// file-based typeRenderTestCase, which no ported test uses). `toCode` is
// awaited because the vendored prettify moved to prettier 3's async format.
export type TypeRenderer = (
type: GraphQLNamedType,
renderContext: RenderContext,
) => void;
export type SchemaRenderer = (
schema: GraphQLSchema,
renderContext: RenderContext,
) => void;
// Round-trips the SDL through introspection, as upstream did, so the
// renderers see a client schema exactly like one built from a real endpoint.
export const toClientSchema = async (
schemaGql: string,
): Promise<GraphQLSchema> => {
const schema = buildSchema(schemaGql);
const introspectionResponse = await graphql({
schema,
source: getIntrospectionQuery(),
});
if (!introspectionResponse.data) {
throw new Error(JSON.stringify(introspectionResponse.errors));
}
return buildClientSchema(
introspectionResponse.data as unknown as IntrospectionQuery,
);
};
export const schemaRenderTest = async (
schemaGql: string,
renderer: SchemaRenderer,
parser?: BuiltInParserName,
): Promise<string> => {
const schema = await toClientSchema(schemaGql);
const renderContext = new RenderContext(schema);
renderer(schema, renderContext);
return await renderContext.toCode(parser, true);
};

View File

@@ -0,0 +1,130 @@
import { type GraphQLObjectType, type GraphQLSchema } from 'graphql';
import { beforeAll, describe, expect, test } from 'vitest';
import { renderTyping } from '../../genql/render/common/renderTyping';
import { toClientSchema } from './render-test-helpers';
// Port of remorses/genql@v3.0.5
// cli/src/render/common/__tests__/renderTyping.test.ts, unchanged apart from
// the vitest imports.
let schema: GraphQLSchema;
const testCase = (
field: string,
expected: string,
undefinableValues: boolean,
undefinableFields: boolean,
) => {
const fields = (schema.getType('Object') as GraphQLObjectType).getFields();
const actual = renderTyping(
fields[field].type,
undefinableValues,
undefinableFields,
);
expect(actual).toBe(expected);
};
describe('renderTyping', () => {
beforeAll(async () => {
schema = await toClientSchema(/* GraphQL */ `
type Object {
value: Int!
maybeValue: Int
valueList: [Int!]!
maybeValueList: [Int]!
valueMaybeList: [Int!]
maybeValueMaybeList: [Int]
nested: [[[Int!]!]!]!
maybeNested: [[[Int]]]
}
type Query {
_: Boolean
}
`);
});
test('nullable', () => {
testCase('value', ": Scalars['Int']", false, false);
testCase('maybeValue', ": (Scalars['Int'] | null)", false, false);
testCase('valueList', ": Scalars['Int'][]", false, false);
testCase('maybeValueList', ": (Scalars['Int'] | null)[]", false, false);
testCase('valueMaybeList', ": (Scalars['Int'][] | null)", false, false);
testCase(
'maybeValueMaybeList',
": ((Scalars['Int'] | null)[] | null)",
false,
false,
);
testCase('nested', ": Scalars['Int'][][][]", false, false);
testCase(
'maybeNested',
": ((((Scalars['Int'] | null)[] | null)[] | null)[] | null)",
false,
false,
);
});
test('undefinable values', () => {
testCase('value', ": Scalars['Int']", true, false);
testCase('maybeValue', ": (Scalars['Int'] | undefined)", true, false);
testCase('valueList', ": Scalars['Int'][]", true, false);
testCase('maybeValueList', ": (Scalars['Int'] | undefined)[]", true, false);
testCase('valueMaybeList', ": (Scalars['Int'][] | undefined)", true, false);
testCase(
'maybeValueMaybeList',
": ((Scalars['Int'] | undefined)[] | undefined)",
true,
false,
);
testCase('nested', ": Scalars['Int'][][][]", true, false);
testCase(
'maybeNested',
": ((((Scalars['Int'] | undefined)[] | undefined)[] | undefined)[] | undefined)",
true,
false,
);
});
test('undefinable fields', () => {
testCase('value', ": Scalars['Int']", false, true);
testCase('maybeValue', "?: (Scalars['Int'] | null)", false, true);
testCase('valueList', ": Scalars['Int'][]", false, true);
testCase('maybeValueList', ": (Scalars['Int'] | null)[]", false, true);
testCase('valueMaybeList', "?: (Scalars['Int'][] | null)", false, true);
testCase(
'maybeValueMaybeList',
"?: ((Scalars['Int'] | null)[] | null)",
false,
true,
);
testCase('nested', ": Scalars['Int'][][][]", false, true);
testCase(
'maybeNested',
"?: ((((Scalars['Int'] | null)[] | null)[] | null)[] | null)",
false,
true,
);
});
test('undefinable fields and values', () => {
testCase('value', ": Scalars['Int']", true, true);
testCase('maybeValue', "?: Scalars['Int']", true, true);
testCase('valueList', ": Scalars['Int'][]", true, true);
testCase('maybeValueList', ": (Scalars['Int'] | undefined)[]", true, true);
testCase('valueMaybeList', "?: Scalars['Int'][]", true, true);
testCase(
'maybeValueMaybeList',
"?: (Scalars['Int'] | undefined)[]",
true,
true,
);
testCase('nested', ": Scalars['Int'][][][]", true, true);
testCase(
'maybeNested',
"?: (((Scalars['Int'] | undefined)[] | undefined)[] | undefined)[]",
true,
true,
);
});
});

View File

@@ -0,0 +1,173 @@
import { type GraphQLNamedType } from 'graphql';
import { expect, test } from 'vitest';
import { RenderContext } from '../../genql/render/common/RenderContext';
import { objectType } from '../../genql/render/typeMap/objectType';
import { scalarType } from '../../genql/render/typeMap/scalarType';
import { unionType } from '../../genql/render/typeMap/unionType';
import { type Type } from '../../genql/runtime/types';
import { toClientSchema } from './render-test-helpers';
// Port of remorses/genql@v3.0.5 cli/src/render/typeMap/index.test.ts. Upstream
// ran these with `output = true`, which skipped the assertions (a leftover
// "TODO readd tests") and let its expected values go stale: an argument's
// entry is `[namedType]`, with the wrapped type string appended only when it
// differs (e.g. `['Int', '[[[Int]]]']`), not always `[namedType, namedType]`.
// This port runs the assertions enabled, with the expected values corrected
// to the engine's actual (and upstream 3.0.5's actual) output.
type Renderer = (
type: GraphQLNamedType,
renderContext: RenderContext,
) => Type<string>;
const testCase = async (
schemaGql: string,
renderer: Renderer,
cases: { [type: string]: Type<string> },
) => {
const schema = await toClientSchema(schemaGql);
const renderContext = new RenderContext(schema);
for (const typeName in cases) {
const type = schema.getType(typeName);
const expected = cases[typeName];
if (!type) {
throw new Error(`type ${typeName} is not defined in the schema`);
}
expect(renderer(type, renderContext), typeName).toEqual(expected);
}
};
test('scalarType', () =>
testCase(
/* GraphQL */ `
enum Enum {
some
other
}
scalar Scalar
type Query {
scalar: String
customScalar: Scalar
enum: Enum
}
`,
scalarType as Renderer,
{
String: {},
Scalar: {},
Enum: {},
},
));
test('objectType', () =>
testCase(
/* GraphQL */ `
interface Interface {
some: String
}
type ImplementorA implements Interface {
some: String
}
type ImplementorB implements Interface {
some: String
}
type Object {
scalar: Int
object: Object
interface: Interface
optionalArgScalar(arg: Int): Int
optionalArgObject(arg: Int): Object
optionalArgInterface(arg: Int): Interface
nestedArg(a: [[[Int]]], b: [[[Int!]!]!]!): Boolean
}
type ObjectWithoutScalar {
object: Object
interface: Interface
}
type Query {
_: Boolean
}
`,
objectType as Renderer,
{
Object: {
scalar: { type: 'Int' },
object: { type: 'Object' },
interface: { type: 'Interface' },
optionalArgScalar: {
type: 'Int',
args: { arg: ['Int'] },
},
optionalArgObject: {
type: 'Object',
args: { arg: ['Int'] },
},
optionalArgInterface: {
type: 'Interface',
args: { arg: ['Int'] },
},
nestedArg: {
type: 'Boolean',
args: {
a: ['Int', '[[[Int]]]'],
b: ['Int', '[[[Int!]!]!]!'],
},
},
__typename: { type: 'String' },
},
Interface: {
some: { type: 'String' },
on_ImplementorA: { type: 'ImplementorA' },
on_ImplementorB: { type: 'ImplementorB' },
__typename: { type: 'String' },
},
ObjectWithoutScalar: {
__typename: { type: 'String' },
interface: { type: 'Interface' },
object: { type: 'Object' },
},
},
));
test('unionType', () =>
testCase(
/* GraphQL */ `
type Some {
field: Int
}
type Other {
field: Int
}
type Another {
field: Int
}
union Union = Some | Other | Another
type Query {
_: Boolean
}
`,
unionType as Renderer,
{
Union: {
on_Some: { type: 'Some' },
on_Other: { type: 'Other' },
on_Another: { type: 'Another' },
__typename: { type: 'String' },
},
},
));

View File

@@ -0,0 +1,466 @@
import { describe, test } from 'vitest';
import { type FieldsSelection } from '../../genql/runtime/typeSelection';
// Port of remorses/genql@v3.0.5 cli/src/typeSelection.test.ts, the type-level
// contract of the runtime's FieldsSelection. Every test body is compile-time
// only (dontExecute); the assertions are the `@ts-expect-error` comments,
// enforced by the package typecheck. Upstream's fully commented-out blocks
// ("hide fields in request", the trailing union experiments) are not ported.
type SRC = {
literalsUnion: 'a' | 'b';
nullableField: null | { x: boolean; optional?: string };
list: {
x: number;
a: string;
optional?: string;
}[];
nested?: {
list?: {
edges?: {
x?: number;
}[];
}[];
};
category: {
a: Date;
b: Date;
c: Date;
nested1: {
a: string;
b: string;
c: string;
};
nested2: {
a: string;
b: string;
};
optionalFieldsNested?: {
a?: string;
b?: number;
};
};
optionalFields: {
a?: string;
b?: number;
};
order: {
customer: {
address: {
city: 1;
a: 1;
b: 1;
};
};
};
union:
| { a: string; __isUnion?: true }
| { a: string; b: string; __isUnion?: true };
nesting: {
nestedUnion:
| { a: string; __isUnion?: true }
| { a: string; b: string; __isUnion?: true };
};
xxx: {
xxx: boolean;
};
yyy: {
yyy: boolean;
};
argumentSyntax: {
a: string;
optional?: string;
nesting: {
x: number;
y: number;
};
union:
| { a: string; __isUnion?: true }
| { a: string; b: string; __isUnion?: true };
list: {
x: number;
a: string;
optional?: string;
}[];
};
argumentScalar?: string;
};
describe('pick', () => {
const req = {
category: {
a: 1,
b: 1,
nested1: {
a: 1,
},
},
argumentSyntax: {
__args: { x: 3 },
a: 1,
nesting: {
__scalar: 1,
},
},
};
const z: FieldsSelection<SRC, NoExtraProperties<typeof req>> = {} as any;
test(
'response type picks from request type',
dontExecute(() => {
z.category;
z.category.a;
z.category.b;
// @ts-expect-error c was not selected
z.category.c;
z.category.nested1.a;
}),
);
test(
'response type does not have additional properties',
dontExecute(() => {
// @ts-expect-error not selected
z.order;
// @ts-expect-error not selected
z.category.nested1.b;
// @ts-expect-error not selected
z.category.nested1.c;
// @ts-expect-error not selected
z.category.nested2;
}),
);
test(
'argument syntax',
dontExecute(() => {
z.argumentSyntax.a.toLocaleLowerCase;
}),
);
});
describe('__scalar', () => {
const req = {
__name: 'name',
category: {
__scalar: 1,
nested1: {
a: 1,
},
},
argumentSyntax: {
__args: { a: 7 },
__scalar: 1,
},
argumentScalar: {
__args: { x: 9 },
},
};
const z: FieldsSelection<SRC, typeof req> = {} as any;
test(
'response type picks from request type',
dontExecute(() => {
z.category;
z.category.a;
z.category.b;
z.category.c;
z.category.nested1.a;
z.category.a.getDate;
z.category.b.getDate;
}),
);
test(
'response type does not have additional properties',
dontExecute(() => {
// @ts-expect-error not selected
z.order;
// @ts-expect-error not selected
z.category.nested1.b;
// @ts-expect-error not selected
z.category.nested1.c;
// @ts-expect-error not selected
z.category.nested2;
}),
);
test(
'__scalar is not present',
dontExecute(() => {
// @ts-expect-error __scalar is stripped from the response
z.category.__scalar;
}),
);
test(
'__name is not present',
dontExecute(() => {
// @ts-expect-error __name is stripped from the response
z.__name;
}),
);
test(
'argument syntax',
dontExecute(() => {
z.argumentSyntax.a.toLocaleLowerCase;
z.argumentSyntax.optional?.big;
// @ts-expect-error nesting is not a scalar, __scalar does not select it
z.argumentSyntax.nesting.x;
}),
);
test(
'argument syntax on scalar',
dontExecute(() => {
z.argumentScalar;
z.argumentScalar?.charAt;
// @ts-expect-error not a string property
z.argumentScalar.xx;
}),
);
});
describe('optional fields', () => {
const req = {
optionalFields: {
a: 1,
b: 1,
},
category: {
optionalFieldsNested: {
__scalar: 1,
},
},
argumentSyntax: {
optional: 1,
},
};
const z: FieldsSelection<SRC, typeof req> = {} as any;
test(
'optional fields are preserved',
dontExecute(() => {
// @ts-expect-error optional
z.optionalFields.a.toLocaleLowerCase;
z.optionalFields.a?.toLocaleLowerCase;
// @ts-expect-error optional
z.optionalFields.b.toLocaleLowerCase;
z.optionalFields.b?.toFixed;
// @ts-expect-error optional
z.category.optionalFieldsNested.a;
// @ts-expect-error optional
z.category?.optionalFieldsNested.a;
}),
);
test(
'optional fields are preserved in __scalar',
dontExecute(() => {
// @ts-expect-error optional
z.optionalFields.a.toLocaleLowerCase;
z.optionalFields.a?.toLocaleLowerCase;
// @ts-expect-error optional
z.optionalFields.b.toLocaleLowerCase;
z.optionalFields.b?.toFixed;
// @ts-expect-error optional
z.category.optionalFieldsNested.a;
z.category.optionalFieldsNested?.a?.toLocaleLowerCase;
}),
);
test(
'argument syntax',
dontExecute(() => {
// @ts-expect-error optional
z.argumentSyntax.optional.toLocaleLowerCase;
z.argumentSyntax.optional?.toLocaleLowerCase;
}),
);
});
describe('unions', () => {
const req = {
union: {
onX: {
a: 1,
__scalar: 1,
},
},
nesting: {
nestedUnion: {
onX: {
a: 1,
},
onY: {
b: 1,
},
},
},
argumentSyntax: {
union: {
a: 1,
onX: {
b: 1,
},
},
},
};
const z: FieldsSelection<SRC, typeof req> = {} as any;
test(
'pick union fields',
dontExecute(() => {
z.union.a.toLocaleLowerCase;
z.union.a.toLocaleLowerCase;
z.nesting.nestedUnion.a.toLocaleLowerCase;
}),
);
test(
'does not have __isUnion',
dontExecute(() => {
// @ts-expect-error __isUnion is stripped from the response
z.union.__isUnion;
// @ts-expect-error __isUnion is stripped from the response
z.nesting.nestedUnion.__isUnion;
}),
);
test(
'argument syntax',
dontExecute(() => {
z.argumentSyntax.union.a.charAt;
// @ts-expect-error a was not selected on argumentSyntax
z.argumentSyntax.a;
}),
);
});
describe('arrays', () => {
const req = {
list: {
a: 1,
x: 1,
optional: 1,
},
nested: {
__args: { x: 1 },
__scalar: 1,
list: {
edges: {
x: 1,
},
},
},
argumentSyntax: {
list: {
x: 1,
optional: 1,
},
},
};
const z: FieldsSelection<SRC, typeof req> = {} as any;
test(
'list',
dontExecute(() => {
z.list[0].a.charCodeAt;
z.list[0].x.toFixed;
}),
);
test(
'nested',
dontExecute(() => {
z.nested?.list?.[0]?.edges?.[0].x?.toFixed;
}),
);
test(
'maintain optionals',
dontExecute(() => {
// @ts-expect-error optional
z.list[0].optional.bold;
z.list[0].optional?.bold;
}),
);
test(
'args syntax',
dontExecute(() => {
z.argumentSyntax.list[0].x;
z.argumentSyntax.list[0].optional?.charAt;
// @ts-expect-error optional
z.argumentSyntax.list[0].optional.charAt;
}),
);
});
describe('literals unions', () => {
const req = {
literalsUnion: 1,
};
const z: FieldsSelection<SRC, typeof req> = {} as any;
test(
'literals',
dontExecute(() => {
z.literalsUnion.blink;
z.literalsUnion === 'a';
z.literalsUnion === 'b';
// @ts-expect-error not a member of the literals union
z.literalsUnion === 'x';
}),
);
});
describe('nullable fields', () => {
const req = {
nullableField: {
x: 1,
optional: 1,
},
};
const z: FieldsSelection<SRC, typeof req> = {} as any;
test(
'accessible',
dontExecute(() => {
z.nullableField.x;
z.nullableField.optional?.big;
// @ts-expect-error optional
z.nullableField.optional.big;
}),
);
});
test(
'complex optional type with array',
dontExecute(() => {
interface ForkConnection {
edges?: (ForkEdge | undefined)[];
__typename?: 'ForkConnection';
}
interface ForkEdge {
cursor?: string;
node?: { x: string; y: string };
nodes?: { x?: string; y?: string }[];
__typename?: 'ForkEdge';
}
type X = FieldsSelection<
ForkConnection | undefined,
{
edges?: {
node: {
x: 1;
};
nodes: {
__scalar: 1;
};
};
}
>;
const x: X = {} as any;
x?.edges?.[0]?.node?.x?.toLocaleLowerCase;
// @ts-expect-error y was not selected
x?.edges?.[0]?.node?.y?.toLocaleLowerCase;
x?.edges?.[0]?.nodes?.[0].x?.toLocaleLowerCase;
x?.edges?.[0]?.nodes?.[0].y?.toLocaleLowerCase;
}),
);
function dontExecute(_f: () => void) {
return () => {};
}
type Impossible<TKeys extends keyof any> = {
[TKey in TKeys]: never;
};
type NoExtraProperties<
TObject,
TExtended extends TObject = TObject,
> = TExtended & Impossible<Exclude<keyof TExtended, keyof TObject>>;

View File

@@ -0,0 +1,51 @@
type ClientWrapperOptions = {
apiClientName: string;
defaultUrl: string;
includeUploadFile: boolean;
};
const STRIPPED_TYPES_START = '// __STRIPPED_DURING_INJECTION_START__';
const STRIPPED_TYPES_END = '// __STRIPPED_DURING_INJECTION_END__';
const UPLOAD_FILE_START = '// __UPLOAD_FILE_START__';
const UPLOAD_FILE_END = '// __UPLOAD_FILE_END__';
const escapeRegExp = (value: string): string =>
value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
export const buildClientWrapperSource = (
templateSource: string,
options: ClientWrapperOptions,
): string => {
let source = templateSource;
source = source.replace(
new RegExp(
`${escapeRegExp(STRIPPED_TYPES_START)}[\\s\\S]*?${escapeRegExp(STRIPPED_TYPES_END)}\\n?`,
),
'',
);
source = source.replace("'__TWENTY_DEFAULT_URL__'", options.defaultUrl);
source = source.replace(/TwentyGeneratedClient/g, options.apiClientName);
if (!options.includeUploadFile) {
source = source.replace(
new RegExp(
`\\s*${escapeRegExp(UPLOAD_FILE_START)}[\\s\\S]*?${escapeRegExp(UPLOAD_FILE_END)}\\n?`,
),
'\n',
);
} else {
source = source.replace(
new RegExp(`\\s*${escapeRegExp(UPLOAD_FILE_START)}\\n`),
'\n',
);
source = source.replace(
new RegExp(`\\s*${escapeRegExp(UPLOAD_FILE_END)}\\n`),
'\n',
);
}
return `\n// ${options.apiClientName} (auto-injected by twenty-client-sdk)\n${source}`;
};

View File

@@ -0,0 +1,41 @@
import { cp, mkdir, readdir, rename as fsRename, rm } from 'node:fs/promises';
import { join } from 'node:path';
export const ensureDir = (dirPath: string) =>
mkdir(dirPath, { recursive: true });
export const emptyDir = async (dirPath: string): Promise<void> => {
let entries: string[];
try {
entries = await readdir(dirPath);
} catch (error: unknown) {
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
await mkdir(dirPath, { recursive: true });
return;
}
throw error;
}
await Promise.all(
entries.map((entry) =>
rm(join(dirPath, entry), { recursive: true, force: true }),
),
);
};
export const move = async (src: string, dest: string): Promise<void> => {
try {
await fsRename(src, dest);
} catch (error: unknown) {
if (error instanceof Error && 'code' in error && error.code === 'EXDEV') {
await cp(src, dest, { recursive: true });
await rm(src, { recursive: true, force: true });
} else {
throw error;
}
}
};
export const remove = (filePath: string) =>
rm(filePath, { recursive: true, force: true });

View File

@@ -0,0 +1,122 @@
import { appendFile, copyFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
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 = {
DateTime: 'string',
JSON: 'Record<string, unknown>',
UUID: 'string',
};
export const GENERATED_CORE_DIR = 'core/generated';
// Generates the core API client from a GraphQL schema string.
// Produces both TypeScript source and compiled ESM/CJS bundles.
export const generateCoreClientFromSchema = async ({
schema,
outputPath,
clientWrapperTemplateSource,
}: {
schema: string;
outputPath: string;
clientWrapperTemplateSource?: string;
}): Promise<void> => {
const templateSource =
clientWrapperTemplateSource ?? twentyClientTemplateSource;
const tempPath = `${outputPath}.tmp`;
await ensureDir(tempPath);
await emptyDir(tempPath);
try {
await generate({
schema,
output: tempPath,
scalarTypes: COMMON_SCALAR_TYPES,
});
const clientContent = buildClientWrapperSource(templateSource, {
apiClientName: 'CoreApiClient',
defaultUrl: `\`\${process.env.${DEFAULT_API_URL_NAME}}/graphql\``,
includeUploadFile: true,
});
await appendFile(join(tempPath, 'index.ts'), clientContent);
await remove(outputPath);
await move(tempPath, outputPath);
await compileGeneratedClient(outputPath);
} catch (error) {
await remove(tempPath);
throw error;
}
};
// Generates the core client and replaces the pre-built stub inside
// an installed twenty-client-sdk package (dist/core.mjs and dist/core.cjs).
// Generated source files are kept in dist/generated-core/ for consumers
// that need the raw .ts files (e.g. the app:dev upload step).
export const replaceCoreClient = async ({
packageRoot,
schema,
}: {
packageRoot: string;
schema: string;
}): Promise<void> => {
const generatedPath = join(packageRoot, 'dist', GENERATED_CORE_DIR);
await generateCoreClientFromSchema({ schema, outputPath: generatedPath });
await copyFile(
join(generatedPath, 'index.mjs'),
join(packageRoot, 'dist', 'core.mjs'),
);
await copyFile(
join(generatedPath, 'index.cjs'),
join(packageRoot, 'dist', 'core.cjs'),
);
};
const compileGeneratedClient = async (generatedDir: string): Promise<void> => {
const entryPoint = join(generatedDir, 'index.ts');
const outfile = join(generatedDir, 'index.mjs');
await build({
entryPoints: [entryPoint],
outfile,
bundle: true,
format: 'esm',
platform: 'node',
target: 'node18',
sourcemap: false,
minify: false,
});
await build({
entryPoints: [entryPoint],
outfile: join(generatedDir, 'index.cjs'),
bundle: true,
format: 'cjs',
platform: 'node',
target: 'node18',
sourcemap: false,
minify: false,
});
await writeFile(
join(generatedDir, 'package.json'),
JSON.stringify(
{ type: 'module', main: 'index.mjs', module: 'index.mjs' },
null,
2,
),
);
};

View File

@@ -0,0 +1,48 @@
import { appendFile } from 'node:fs/promises';
import { join } from 'node:path';
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 = {
DateTime: 'string',
JSON: 'Record<string, unknown>',
UUID: 'string',
};
export const generateMetadataClient = async ({
schema,
outputPath,
clientWrapperTemplateSource,
}: {
schema: string;
outputPath: string;
clientWrapperTemplateSource?: string;
}): Promise<void> => {
const templateSource =
clientWrapperTemplateSource ?? twentyClientTemplateSource;
await ensureDir(outputPath);
await emptyDir(outputPath);
await generate({
schema,
output: outputPath,
scalarTypes: {
...COMMON_SCALAR_TYPES,
Upload: 'File',
},
});
const clientContent = buildClientWrapperSource(templateSource, {
apiClientName: 'MetadataApiClient',
defaultUrl: `\`\${process.env.${DEFAULT_API_URL_NAME}}/metadata\``,
includeUploadFile: true,
});
await appendFile(join(outputPath, 'index.ts'), clientContent);
};

View File

@@ -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.

View File

@@ -0,0 +1,52 @@
# Vendored genql codegen
This folder is a narrowed, vendored copy of [`@genql/cli`](https://github.com/remorses/genql)
`3.0.5` (upstream commit `4a547db46a9a614cc2b5958e28674af351898464`; MIT,
© Tommaso De Rossi "morse"), used to generate the typed GraphQL client from an
SDL string.
Upstream's own test suite (integration + unit) is ported under
`../__tests__/upstream-3.0.5/` and pins the engine's behavior and generated
types; the snapshot tests (`../__tests__/genql-engine-output.test.ts` and the
ported suite's checked-in fixture client) pin its output byte-for-byte.
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 renderers are vendored verbatim from `@genql/cli@3.0.5`. Formatting now runs
on `prettier@^3` (the version the monorepo resolves): it needs the explicit
`prettier/plugins/estree` printer and an awaited, async `format()`. 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`.

View File

@@ -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;
}

View File

@@ -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);
};

View File

@@ -0,0 +1,29 @@
// @ts-nocheck
import prettier from 'prettier/standalone'
import { BuiltInParserName } from 'prettier'
import * as parserGraphql from 'prettier/plugins/graphql'
import * as parserTS from 'prettier/plugins/typescript'
import * as parserEstree from 'prettier/plugins/estree'
// Prettier 3 split the estree printer out of the TS parser and made format()
// async. Without prettier/plugins/estree the standalone bundle throws
// "Couldn't find plugin for AST format estree", which crashed the server-side
// GqlTypeGenerator during app sync. Formatting is best-effort cosmetic, so we
// fall back to the unformatted (still valid) code rather than ever throwing.
export const prettify = async (
code: string,
parser?: BuiltInParserName,
): Promise<string> => {
try {
return await prettier.format(code, {
parser,
plugins: [parserGraphql, parserTS, parserEstree],
semi: false,
singleQuote: true,
trailingComma: 'all',
printWidth: 80,
})
} catch {
return code
}
}

View File

@@ -0,0 +1,2 @@
export { generate } from './main';
export { type Config } from './config';

View File

@@ -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);
};

View File

@@ -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}
}
`
}

View File

@@ -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
}
async 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('')
}
}

View File

@@ -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,
})

View File

@@ -0,0 +1,11 @@
// @ts-nocheck
export const excludedTypes = [
'__Schema',
'__Type',
'__TypeKind',
'__Field',
'__InputValue',
'__EnumValue',
'__Directive',
'__DirectiveLocation',
]

View File

@@ -0,0 +1,7 @@
// @ts-nocheck
import { relative } from 'node:path'
export const relativeImportPath = (from: string, to: string) => {
const fromResolved = relative(from, to)
return fromResolved[0] === '.' ? fromResolved : `./${fromResolved}`
}

View File

@@ -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)

View File

@@ -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)))
}

View File

@@ -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(',')}}`)
}

View File

@@ -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}}`
}

View File

@@ -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 ''
}

View File

@@ -0,0 +1,7 @@
// @ts-nocheck
import { GraphQLNamedType } from 'graphql'
export const requestTypeName = (type?: GraphQLNamedType) => {
if (!type) return ''
return `${type.name}GenqlSelection`
}

View File

@@ -0,0 +1,29 @@
// @ts-nocheck
import { GraphQLUnionType } from 'graphql'
import uniq from 'lodash/uniq.js'
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}`,
)
}

View File

@@ -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(' | ')}`)
}

View File

@@ -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
// }`,
// )
// }

View File

@@ -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}`,
)
}

View File

@@ -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 ''
}

View File

@@ -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}}`
}

View File

@@ -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}`,
// )
// }
// }

View File

@@ -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
// }`,
// )
// }

View File

@@ -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))
}

View File

@@ -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]))
}
}
}

View File

@@ -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
}

View File

@@ -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,
}
}

View File

@@ -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> => ({})

View File

@@ -0,0 +1,7 @@
// @ts-nocheck
export function isEmpty(x) {
if (!x) {
return true
}
return Object.keys(x).length === 0
}

View File

@@ -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.js'
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
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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>
}

View File

@@ -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),
)
}
}

View File

@@ -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
}

View File

@@ -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,
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -0,0 +1,106 @@
import { GraphQLEnumType, GraphQLSchema, isEnumType } from 'graphql';
import camelCase from 'lodash/camelCase.js';
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],
await 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' + (await schemaTypesCtx.toCode('typescript')),
);
const typeMapCtx = new RenderContext(schema, config);
renderTypeMap(schema, typeMapCtx);
await writeFileToPath(
[output, typeMapFileEsm],
`export default ${await typeMapCtx.toCode()}`,
);
const clientCtx = new RenderContext(schema, config);
renderClientEsm(schema, clientCtx);
await writeFileToPath(
[output, clientFileEsm],
'// @ts-nocheck\n' + (await 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'),
);
}

View File

@@ -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;
};

View File

@@ -0,0 +1,6 @@
export {
GENERATED_CORE_DIR,
generateCoreClientFromSchema,
replaceCoreClient,
} from './generate-core-client';
export { generateMetadataClient } from './generate-metadata-client';

View File

@@ -0,0 +1,4 @@
declare module '*.ts?raw' {
const content: string;
export default content;
}

View File

@@ -0,0 +1,66 @@
// The generated client ships its own copy of the genql runtime. We import the
// files as raw text (Vite's `?raw`) 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).
//
// `?raw` only resolves under Vite. When this module runs outside a Vite bundle
// — e.g. `tsx scripts/generate-metadata-client.ts`, which backs the
// `generate-metadata-client` target and server-validation — the `?raw` imports
// are `undefined`, so we fall back to reading the source files from
// disk. In the bundled build the imports are defined and the fallback is never
// reached, so the shipped runtime is unchanged.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import batcher from './genql/runtime/batcher.ts?raw';
import createClient from './genql/runtime/createClient.ts?raw';
import error from './genql/runtime/error.ts?raw';
import fetcher from './genql/runtime/fetcher.ts?raw';
import generateGraphqlOperation from './genql/runtime/generateGraphqlOperation.ts?raw';
import index from './genql/runtime/index.ts?raw';
import linkTypeMap from './genql/runtime/linkTypeMap.ts?raw';
import typeSelection from './genql/runtime/typeSelection.ts?raw';
import types from './genql/runtime/types.ts?raw';
const readTemplate = (
bundled: string | undefined,
fileName: string,
): string => {
if (bundled !== undefined) return bundled;
const runtimeDir = join(
dirname(fileURLToPath(import.meta.url)),
'genql',
'runtime',
);
return readFileSync(join(runtimeDir, fileName), 'utf-8');
};
export const RUNTIME_TEMPLATE_FILES: { name: string; content: string }[] = [
{ name: 'batcher.ts', content: readTemplate(batcher, 'batcher.ts') },
{
name: 'createClient.ts',
content: readTemplate(createClient, 'createClient.ts'),
},
{ name: 'error.ts', content: readTemplate(error, 'error.ts') },
{ name: 'fetcher.ts', content: readTemplate(fetcher, 'fetcher.ts') },
{
name: 'generateGraphqlOperation.ts',
content: readTemplate(
generateGraphqlOperation,
'generateGraphqlOperation.ts',
),
},
{ name: 'index.ts', content: readTemplate(index, 'index.ts') },
{
name: 'linkTypeMap.ts',
content: readTemplate(linkTypeMap, 'linkTypeMap.ts'),
},
{
name: 'typeSelection.ts',
content: readTemplate(typeSelection, 'typeSelection.ts'),
},
{ name: 'types.ts', content: readTemplate(types, 'types.ts') },
];

View File

@@ -0,0 +1,446 @@
import type { TwentyClientRunAs } from '../shared/twenty-client-run-as.type';
// Ambient type stubs for the genql-generated code this template gets
// injected into. They enable full typecheck/lint on this file.
// __STRIPPED_DURING_INJECTION_START__
type QueryGenqlSelection = Record<string, unknown>;
type MutationGenqlSelection = Record<string, unknown>;
type GraphqlOperation = Record<string, unknown>;
type ClientOptions = Omit<RequestInit, 'body' | 'headers'> & {
url?: string;
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
fetcher?: (
operation: GraphqlOperation | GraphqlOperation[],
) => Promise<unknown>;
fetch?: typeof globalThis.fetch;
batch?: unknown;
};
type Client = {
query: (
request: QueryGenqlSelection & { __name?: string },
) => Promise<unknown>;
mutation: (
request: MutationGenqlSelection & { __name?: string },
) => Promise<unknown>;
};
declare function createClient(options: ClientOptions): Client;
declare class GenqlError extends Error {
constructor(errors: unknown, data: unknown);
}
// __STRIPPED_DURING_INJECTION_END__
const APP_ACCESS_TOKEN_ENV_KEY = 'TWENTY_APP_ACCESS_TOKEN';
const APP_APPLICATION_ACCESS_TOKEN_ENV_KEY =
'TWENTY_APP_APPLICATION_ACCESS_TOKEN';
const API_KEY_ENV_KEY = 'TWENTY_API_KEY';
export type TwentyGeneratedClientOptions = ClientOptions & {
runAs?: TwentyClientRunAs;
};
type ProcessEnvironment = Record<string, string | undefined>;
type GraphqlErrorPayloadEntry = {
message?: string;
extensions?: { code?: string };
};
type GraphqlResponsePayload = {
data?: Record<string, unknown>;
errors?: GraphqlErrorPayloadEntry[];
};
type GraphqlResponse = {
status: number;
statusText: string;
payload: GraphqlResponsePayload | null;
rawBody: string;
};
const getProcessEnvironment = (): ProcessEnvironment => {
const processObject = (
globalThis as { process?: { env?: ProcessEnvironment } }
).process;
return processObject?.env ?? {};
};
const getTokenFromAuthorizationHeader = (
authorizationHeader: string | undefined,
): string | null => {
if (typeof authorizationHeader !== 'string') {
return null;
}
const trimmedAuthorizationHeader = authorizationHeader.trim();
if (trimmedAuthorizationHeader.length === 0) {
return null;
}
if (trimmedAuthorizationHeader === 'Bearer') {
return null;
}
if (trimmedAuthorizationHeader.startsWith('Bearer ')) {
return trimmedAuthorizationHeader.slice('Bearer '.length).trim();
}
return trimmedAuthorizationHeader;
};
const getTokenFromHeaders = (
headers: HeadersInit | undefined,
): string | null => {
if (!headers) {
return null;
}
if (headers instanceof Headers) {
return getTokenFromAuthorizationHeader(
headers.get('Authorization') ?? undefined,
);
}
if (Array.isArray(headers)) {
const matchedAuthorizationHeader = headers.find(
([headerName]) => headerName.toLowerCase() === 'authorization',
);
return getTokenFromAuthorizationHeader(matchedAuthorizationHeader?.[1]);
}
const headersRecord = headers as Record<string, string | undefined>;
return getTokenFromAuthorizationHeader(
headersRecord.Authorization ?? headersRecord.authorization,
);
};
const hasAuthenticationErrorInGraphqlPayload = (
payload: GraphqlResponsePayload | null,
): boolean => {
if (!payload?.errors) {
return false;
}
return payload.errors.some((graphqlError) => {
return (
graphqlError.extensions?.code === 'UNAUTHENTICATED' ||
graphqlError.message?.toLowerCase() === 'unauthorized'
);
});
};
const defaultOptions: TwentyGeneratedClientOptions = {
url: '__TWENTY_DEFAULT_URL__',
headers: {
'Content-Type': 'application/json',
},
};
export class TwentyGeneratedClient {
private client: Client;
private url: string;
private requestOptions: RequestInit;
private headers: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
private fetchImplementation: typeof globalThis.fetch | null;
private authorizationToken: string | null;
private refreshAccessTokenPromise: Promise<string | null> | null = null;
constructor(options?: TwentyGeneratedClientOptions) {
const merged: TwentyGeneratedClientOptions = {
...defaultOptions,
...options,
};
const {
url,
headers,
fetch: customFetchImplementation,
fetcher: _fetcher,
batch: _batch,
runAs,
...requestOptions
} = merged;
this.url = url ?? '';
this.requestOptions = requestOptions;
this.headers = headers ?? {};
this.fetchImplementation =
customFetchImplementation ?? globalThis.fetch ?? null;
const processEnvironment = getProcessEnvironment();
const tokenFromHeaders = getTokenFromHeaders(
typeof headers === 'function' ? undefined : headers,
);
// Priority: explicit header > the token for the requested access > api key
// (legacy).
this.authorizationToken =
tokenFromHeaders ??
processEnvironment[
runAs === 'application'
? APP_APPLICATION_ACCESS_TOKEN_ENV_KEY
: APP_ACCESS_TOKEN_ENV_KEY
] ??
processEnvironment[API_KEY_ENV_KEY] ??
null;
this.client = createClient({
...merged,
headers: undefined,
fetcher: async (operation) =>
this.executeGraphqlRequestWithOptionalRefresh({
operation,
}),
});
}
query<R extends QueryGenqlSelection>(request: R & { __name?: string }) {
return this.client.query(request);
}
mutation<R extends MutationGenqlSelection>(request: R & { __name?: string }) {
return this.client.mutation(request);
}
// __UPLOAD_FILE_START__
async uploadFile(
fileBuffer: Buffer,
filename: string,
contentType: string = 'application/octet-stream',
fieldMetadataUniversalIdentifier: string,
): Promise<{
id: string;
path: string;
size: number;
createdAt: string;
url: string;
}> {
const form = new FormData();
form.append(
'operations',
JSON.stringify({
query: `mutation UploadFilesFieldFileByUniversalIdentifier($file: Upload!, $fieldMetadataUniversalIdentifier: String!) {
uploadFilesFieldFileByUniversalIdentifier(file: $file, fieldMetadataUniversalIdentifier: $fieldMetadataUniversalIdentifier) { id path size createdAt url }
}`,
variables: {
file: null,
fieldMetadataUniversalIdentifier,
},
}),
);
form.append('map', JSON.stringify({ '0': ['variables.file'] }));
form.append(
'0',
new Blob([fileBuffer as BlobPart], { type: contentType }),
filename,
);
const result = await this.executeGraphqlRequestWithOptionalRefresh({
operation: form,
headers: {},
requestInit: {
method: 'POST',
},
});
if (result.errors) {
throw new GenqlError(result.errors, result.data);
}
const data = result.data as Record<string, unknown>;
return data.uploadFilesFieldFileByUniversalIdentifier as {
id: string;
path: string;
size: number;
createdAt: string;
url: string;
};
}
// __UPLOAD_FILE_END__
private async executeGraphqlRequestWithOptionalRefresh({
operation,
headers,
requestInit,
}: {
operation: GraphqlOperation | GraphqlOperation[] | FormData;
headers?: HeadersInit;
requestInit?: RequestInit;
}) {
const firstResponse = await this.executeGraphqlRequest({
operation,
headers,
requestInit,
token: this.authorizationToken,
});
if (this.shouldRefreshToken(firstResponse)) {
const refreshedAccessToken = await this.requestRefreshedAccessToken();
if (refreshedAccessToken) {
const retryResponse = await this.executeGraphqlRequest({
operation,
headers,
requestInit,
token: refreshedAccessToken,
});
return this.assertResponseIsSuccessful(retryResponse);
}
}
return this.assertResponseIsSuccessful(firstResponse);
}
private async executeGraphqlRequest({
operation,
headers,
requestInit,
token,
}: {
operation: GraphqlOperation | GraphqlOperation[] | FormData;
headers?: HeadersInit;
requestInit?: RequestInit;
token: string | null;
}): Promise<GraphqlResponse> {
if (!this.fetchImplementation) {
throw new Error(
'Global `fetch` function is not available, ' +
'pass a fetch implementation to the Twenty client',
);
}
const resolvedHeaders = await this.resolveHeaders();
const requestHeaders = new Headers(resolvedHeaders);
if (headers) {
new Headers(headers).forEach((value, key) =>
requestHeaders.set(key, value),
);
}
if (operation instanceof FormData) {
requestHeaders.delete('Content-Type');
} else {
requestHeaders.set('Content-Type', 'application/json');
}
if (token) {
requestHeaders.set('Authorization', `Bearer ${token}`);
} else {
requestHeaders.delete('Authorization');
}
const response = await this.fetchImplementation.call(globalThis, this.url, {
...this.requestOptions,
...requestInit,
method: requestInit?.method ?? 'POST',
headers: requestHeaders,
body:
operation instanceof FormData ? operation : JSON.stringify(operation),
});
const rawBody = await response.text();
let payload: GraphqlResponsePayload | null = null;
if (rawBody.trim().length > 0) {
try {
payload = JSON.parse(rawBody) as GraphqlResponsePayload;
} catch {
payload = null;
}
}
return {
status: response.status,
statusText: response.statusText,
payload,
rawBody,
};
}
private async resolveHeaders(): Promise<HeadersInit> {
if (typeof this.headers === 'function') {
return (await this.headers()) ?? {};
}
return this.headers ?? {};
}
private shouldRefreshToken(response: GraphqlResponse): boolean {
if (response.status === 401) {
return true;
}
return hasAuthenticationErrorInGraphqlPayload(response.payload);
}
private assertResponseIsSuccessful(response: GraphqlResponse) {
if (response.status < 200 || response.status >= 300) {
throw new Error(`${response.statusText}: ${response.rawBody}`);
}
if (response.payload === null) {
throw new Error('Invalid JSON response');
}
return response.payload;
}
private async requestRefreshedAccessToken(): Promise<string | null> {
const refreshAccessTokenFunction = (
globalThis as {
frontComponentHostCommunicationApi?: {
requestAccessTokenRefresh?: () => Promise<string>;
};
}
).frontComponentHostCommunicationApi?.requestAccessTokenRefresh;
if (typeof refreshAccessTokenFunction !== 'function') {
return null;
}
if (!this.refreshAccessTokenPromise) {
this.refreshAccessTokenPromise = refreshAccessTokenFunction()
.then((refreshedAccessToken) => {
if (
typeof refreshedAccessToken !== 'string' ||
refreshedAccessToken.length === 0
) {
return null;
}
this.setAuthorizationToken(refreshedAccessToken);
return refreshedAccessToken;
})
.catch((refreshError: unknown) => {
console.error('Twenty client: token refresh failed', refreshError);
return null;
})
.finally(() => {
this.refreshAccessTokenPromise = null;
});
}
return this.refreshAccessTokenPromise;
}
private setAuthorizationToken(token: string) {
this.authorizationToken = token;
const processEnvironment = getProcessEnvironment();
processEnvironment[APP_ACCESS_TOKEN_ENV_KEY] = token;
}
}

View File

@@ -0,0 +1,482 @@
// @ts-nocheck
import type {QueryGenqlSelection,Query,MutationGenqlSelection,Mutation,SubscriptionGenqlSelection,Subscription} from './schema'
import {
linkTypeMap,
createClient as createClientOriginal,
generateGraphqlOperation,
type FieldsSelection, type GraphqlOperation, type ClientOptions, GenqlError
} from './runtime'
export type { FieldsSelection } from './runtime'
export { GenqlError }
import types from './types'
export * from './schema'
const typeMap = linkTypeMap(types as any)
export interface Client {
query<R extends QueryGenqlSelection>(
request: R & { __name?: string },
): Promise<FieldsSelection<Query, R>>
mutation<R extends MutationGenqlSelection>(
request: R & { __name?: string },
): Promise<FieldsSelection<Mutation, R>>
}
export const createClient =
function(options?: ClientOptions): Client {
return createClientOriginal({
url: undefined,
...options,
queryRoot: typeMap.Query!,
mutationRoot: typeMap.Mutation!,
subscriptionRoot: typeMap.Subscription!,
}) as any
}
export const everything = {
__scalar: true
}
export type QueryResult<fields extends QueryGenqlSelection> = FieldsSelection<Query, fields>
export const generateQueryOp: (fields: QueryGenqlSelection & { __name?: string }) => GraphqlOperation = function(fields) {
return generateGraphqlOperation('query', typeMap.Query!, fields as any)
}
export type MutationResult<fields extends MutationGenqlSelection> = FieldsSelection<Mutation, fields>
export const generateMutationOp: (fields: MutationGenqlSelection & { __name?: string }) => GraphqlOperation = function(fields) {
return generateGraphqlOperation('mutation', typeMap.Mutation!, fields as any)
}
export type SubscriptionResult<fields extends SubscriptionGenqlSelection> = FieldsSelection<Subscription, fields>
export const generateSubscriptionOp: (fields: SubscriptionGenqlSelection & { __name?: string }) => GraphqlOperation = function(fields) {
return generateGraphqlOperation('subscription', typeMap.Subscription!, fields as any)
}
// MetadataApiClient (auto-injected by twenty-client-sdk)
import type { TwentyClientRunAs } from '../shared/twenty-client-run-as.type';
// Ambient type stubs for the genql-generated code this template gets
// injected into. They enable full typecheck/lint on this file.
const APP_ACCESS_TOKEN_ENV_KEY = 'TWENTY_APP_ACCESS_TOKEN';
const APP_APPLICATION_ACCESS_TOKEN_ENV_KEY =
'TWENTY_APP_APPLICATION_ACCESS_TOKEN';
const API_KEY_ENV_KEY = 'TWENTY_API_KEY';
export type MetadataApiClientOptions = ClientOptions & {
runAs?: TwentyClientRunAs;
};
type ProcessEnvironment = Record<string, string | undefined>;
type GraphqlErrorPayloadEntry = {
message?: string;
extensions?: { code?: string };
};
type GraphqlResponsePayload = {
data?: Record<string, unknown>;
errors?: GraphqlErrorPayloadEntry[];
};
type GraphqlResponse = {
status: number;
statusText: string;
payload: GraphqlResponsePayload | null;
rawBody: string;
};
const getProcessEnvironment = (): ProcessEnvironment => {
const processObject = (
globalThis as { process?: { env?: ProcessEnvironment } }
).process;
return processObject?.env ?? {};
};
const getTokenFromAuthorizationHeader = (
authorizationHeader: string | undefined,
): string | null => {
if (typeof authorizationHeader !== 'string') {
return null;
}
const trimmedAuthorizationHeader = authorizationHeader.trim();
if (trimmedAuthorizationHeader.length === 0) {
return null;
}
if (trimmedAuthorizationHeader === 'Bearer') {
return null;
}
if (trimmedAuthorizationHeader.startsWith('Bearer ')) {
return trimmedAuthorizationHeader.slice('Bearer '.length).trim();
}
return trimmedAuthorizationHeader;
};
const getTokenFromHeaders = (
headers: HeadersInit | undefined,
): string | null => {
if (!headers) {
return null;
}
if (headers instanceof Headers) {
return getTokenFromAuthorizationHeader(
headers.get('Authorization') ?? undefined,
);
}
if (Array.isArray(headers)) {
const matchedAuthorizationHeader = headers.find(
([headerName]) => headerName.toLowerCase() === 'authorization',
);
return getTokenFromAuthorizationHeader(matchedAuthorizationHeader?.[1]);
}
const headersRecord = headers as Record<string, string | undefined>;
return getTokenFromAuthorizationHeader(
headersRecord.Authorization ?? headersRecord.authorization,
);
};
const hasAuthenticationErrorInGraphqlPayload = (
payload: GraphqlResponsePayload | null,
): boolean => {
if (!payload?.errors) {
return false;
}
return payload.errors.some((graphqlError) => {
return (
graphqlError.extensions?.code === 'UNAUTHENTICATED' ||
graphqlError.message?.toLowerCase() === 'unauthorized'
);
});
};
const defaultOptions: MetadataApiClientOptions = {
url: `${process.env.TWENTY_API_URL}/metadata`,
headers: {
'Content-Type': 'application/json',
},
};
export class MetadataApiClient {
private client: Client;
private url: string;
private requestOptions: RequestInit;
private headers: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
private fetchImplementation: typeof globalThis.fetch | null;
private authorizationToken: string | null;
private refreshAccessTokenPromise: Promise<string | null> | null = null;
constructor(options?: MetadataApiClientOptions) {
const merged: MetadataApiClientOptions = {
...defaultOptions,
...options,
};
const {
url,
headers,
fetch: customFetchImplementation,
fetcher: _fetcher,
batch: _batch,
runAs,
...requestOptions
} = merged;
this.url = url ?? '';
this.requestOptions = requestOptions;
this.headers = headers ?? {};
this.fetchImplementation =
customFetchImplementation ?? globalThis.fetch ?? null;
const processEnvironment = getProcessEnvironment();
const tokenFromHeaders = getTokenFromHeaders(
typeof headers === 'function' ? undefined : headers,
);
// Priority: explicit header > the token for the requested access > api key
// (legacy).
this.authorizationToken =
tokenFromHeaders ??
processEnvironment[
runAs === 'application'
? APP_APPLICATION_ACCESS_TOKEN_ENV_KEY
: APP_ACCESS_TOKEN_ENV_KEY
] ??
processEnvironment[API_KEY_ENV_KEY] ??
null;
this.client = createClient({
...merged,
headers: undefined,
fetcher: async (operation) =>
this.executeGraphqlRequestWithOptionalRefresh({
operation,
}),
});
}
query<R extends QueryGenqlSelection>(request: R & { __name?: string }) {
return this.client.query(request);
}
mutation<R extends MutationGenqlSelection>(request: R & { __name?: string }) {
return this.client.mutation(request);
}
async uploadFile(
fileBuffer: Buffer,
filename: string,
contentType: string = 'application/octet-stream',
fieldMetadataUniversalIdentifier: string,
): Promise<{
id: string;
path: string;
size: number;
createdAt: string;
url: string;
}> {
const form = new FormData();
form.append(
'operations',
JSON.stringify({
query: `mutation UploadFilesFieldFileByUniversalIdentifier($file: Upload!, $fieldMetadataUniversalIdentifier: String!) {
uploadFilesFieldFileByUniversalIdentifier(file: $file, fieldMetadataUniversalIdentifier: $fieldMetadataUniversalIdentifier) { id path size createdAt url }
}`,
variables: {
file: null,
fieldMetadataUniversalIdentifier,
},
}),
);
form.append('map', JSON.stringify({ '0': ['variables.file'] }));
form.append(
'0',
new Blob([fileBuffer as BlobPart], { type: contentType }),
filename,
);
const result = await this.executeGraphqlRequestWithOptionalRefresh({
operation: form,
headers: {},
requestInit: {
method: 'POST',
},
});
if (result.errors) {
throw new GenqlError(result.errors, result.data);
}
const data = result.data as Record<string, unknown>;
return data.uploadFilesFieldFileByUniversalIdentifier as {
id: string;
path: string;
size: number;
createdAt: string;
url: string;
};
}
private async executeGraphqlRequestWithOptionalRefresh({
operation,
headers,
requestInit,
}: {
operation: GraphqlOperation | GraphqlOperation[] | FormData;
headers?: HeadersInit;
requestInit?: RequestInit;
}) {
const firstResponse = await this.executeGraphqlRequest({
operation,
headers,
requestInit,
token: this.authorizationToken,
});
if (this.shouldRefreshToken(firstResponse)) {
const refreshedAccessToken = await this.requestRefreshedAccessToken();
if (refreshedAccessToken) {
const retryResponse = await this.executeGraphqlRequest({
operation,
headers,
requestInit,
token: refreshedAccessToken,
});
return this.assertResponseIsSuccessful(retryResponse);
}
}
return this.assertResponseIsSuccessful(firstResponse);
}
private async executeGraphqlRequest({
operation,
headers,
requestInit,
token,
}: {
operation: GraphqlOperation | GraphqlOperation[] | FormData;
headers?: HeadersInit;
requestInit?: RequestInit;
token: string | null;
}): Promise<GraphqlResponse> {
if (!this.fetchImplementation) {
throw new Error(
'Global `fetch` function is not available, ' +
'pass a fetch implementation to the Twenty client',
);
}
const resolvedHeaders = await this.resolveHeaders();
const requestHeaders = new Headers(resolvedHeaders);
if (headers) {
new Headers(headers).forEach((value, key) =>
requestHeaders.set(key, value),
);
}
if (operation instanceof FormData) {
requestHeaders.delete('Content-Type');
} else {
requestHeaders.set('Content-Type', 'application/json');
}
if (token) {
requestHeaders.set('Authorization', `Bearer ${token}`);
} else {
requestHeaders.delete('Authorization');
}
const response = await this.fetchImplementation.call(globalThis, this.url, {
...this.requestOptions,
...requestInit,
method: requestInit?.method ?? 'POST',
headers: requestHeaders,
body:
operation instanceof FormData ? operation : JSON.stringify(operation),
});
const rawBody = await response.text();
let payload: GraphqlResponsePayload | null = null;
if (rawBody.trim().length > 0) {
try {
payload = JSON.parse(rawBody) as GraphqlResponsePayload;
} catch {
payload = null;
}
}
return {
status: response.status,
statusText: response.statusText,
payload,
rawBody,
};
}
private async resolveHeaders(): Promise<HeadersInit> {
if (typeof this.headers === 'function') {
return (await this.headers()) ?? {};
}
return this.headers ?? {};
}
private shouldRefreshToken(response: GraphqlResponse): boolean {
if (response.status === 401) {
return true;
}
return hasAuthenticationErrorInGraphqlPayload(response.payload);
}
private assertResponseIsSuccessful(response: GraphqlResponse) {
if (response.status < 200 || response.status >= 300) {
throw new Error(`${response.statusText}: ${response.rawBody}`);
}
if (response.payload === null) {
throw new Error('Invalid JSON response');
}
return response.payload;
}
private async requestRefreshedAccessToken(): Promise<string | null> {
const refreshAccessTokenFunction = (
globalThis as {
frontComponentHostCommunicationApi?: {
requestAccessTokenRefresh?: () => Promise<string>;
};
}
).frontComponentHostCommunicationApi?.requestAccessTokenRefresh;
if (typeof refreshAccessTokenFunction !== 'function') {
return null;
}
if (!this.refreshAccessTokenPromise) {
this.refreshAccessTokenPromise = refreshAccessTokenFunction()
.then((refreshedAccessToken) => {
if (
typeof refreshedAccessToken !== 'string' ||
refreshedAccessToken.length === 0
) {
return null;
}
this.setAuthorizationToken(refreshedAccessToken);
return refreshedAccessToken;
})
.catch((refreshError: unknown) => {
console.error('Twenty client: token refresh failed', refreshError);
return null;
})
.finally(() => {
this.refreshAccessTokenPromise = null;
});
}
return this.refreshAccessTokenPromise;
}
private setAuthorizationToken(token: string) {
this.authorizationToken = token;
const processEnvironment = getProcessEnvironment();
processEnvironment[APP_ACCESS_TOKEN_ENV_KEY] = token;
}
}

View File

@@ -0,0 +1,271 @@
// @ts-nocheck
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
}
}

View File

@@ -0,0 +1,68 @@
// @ts-nocheck
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
}

View File

@@ -0,0 +1,29 @@
// @ts-nocheck
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>
}

View File

@@ -0,0 +1,98 @@
// @ts-nocheck
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),
)
}
}

View File

@@ -0,0 +1,225 @@
// @ts-nocheck
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
}

View File

@@ -0,0 +1,13 @@
// @ts-nocheck
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,
}

View File

@@ -0,0 +1,139 @@
// @ts-nocheck
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
}

View File

@@ -0,0 +1,98 @@
// @ts-nocheck
//////////////////////////////////////////////////
// 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

View File

@@ -0,0 +1,69 @@
// @ts-nocheck
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
}

Some files were not shown because too many files have changed in this diff Show More