Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion graphql/codegen/src/cli/commands/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface GenerateOptions {
output?: string;
/** Authorization header */
authorization?: string;
/** Additional HTTP headers for endpoint requests */
headers?: Record<string, string>;
/** Verbose output */
verbose?: boolean;
/** Dry run - don't write files */
Expand Down Expand Up @@ -93,7 +95,7 @@ export async function generateCommand(
endpoint: config.endpoint || undefined,
schema: config.schema || undefined,
authorization: options.authorization || config.headers['Authorization'],
headers: config.headers,
headers: options.headers || config.headers,
});

// 3. Run the codegen pipeline
Expand Down
70 changes: 48 additions & 22 deletions graphql/codegen/src/cli/introspect/fetch-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
* Fetch GraphQL schema introspection from an endpoint
*/
import { SCHEMA_INTROSPECTION_QUERY } from './schema-query';
import * as http from 'node:http';
import * as https from 'node:https';
import { URL } from 'node:url';
import type { IntrospectionQueryResponse } from '../../types/introspection';

export interface FetchSchemaOptions {
Expand Down Expand Up @@ -41,59 +44,82 @@ export async function fetchSchema(
requestHeaders['Authorization'] = authorization;
}

// Create abort controller for timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);

try {
const response = await fetch(endpoint, {
const url = new URL(endpoint);
const isHttps = url.protocol === 'https:';
const client = isHttps ? https : http;
const body = JSON.stringify({ query: SCHEMA_INTROSPECTION_QUERY, variables: {} });
const reqHeaders: Record<string, string | number> = {
...requestHeaders,
'Content-Length': Buffer.byteLength(body),
};

const reqOptions: http.RequestOptions = {
protocol: url.protocol,
hostname: url.hostname,
port: url.port ? Number(url.port) : isHttps ? 443 : 80,
path: url.pathname + url.search,
method: 'POST',
headers: requestHeaders,
body: JSON.stringify({
query: SCHEMA_INTROSPECTION_QUERY,
variables: {},
}),
signal: controller.signal,
headers: reqHeaders,
};

const jsonResult = await new Promise<{ statusCode: number; json: any }>((resolve, reject) => {
const req = client.request(reqOptions, (res) => {
const chunks: Buffer[] = [];
res.on('data', (c) => chunks.push(c as Buffer));
res.on('end', () => {
try {
const text = Buffer.concat(chunks).toString('utf8');
const parsed = text ? JSON.parse(text) : {};
resolve({ statusCode: res.statusCode || 0, json: parsed });
} catch (e) {
reject(e);
}
});
});
req.on('error', reject);
req.setTimeout(timeout, () => {
req.destroy(new Error(`Request timeout after ${timeout}ms`));
});
req.write(body);
req.end();
});

clearTimeout(timeoutId);

if (!response.ok) {
const { statusCode = 0, json } = jsonResult;
if (statusCode < 200 || statusCode >= 300) {
return {
success: false,
error: `HTTP ${response.status}: ${response.statusText}`,
statusCode: response.status,
error: `HTTP ${statusCode}: ${json?.errors?.[0]?.message || 'Request failed'}`,
statusCode,
};
}

const json = (await response.json()) as {
data?: IntrospectionQueryResponse;
errors?: Array<{ message: string }>;
};

// Check for GraphQL errors
if (json.errors && json.errors.length > 0) {
const errorMessages = json.errors.map((e) => e.message).join('; ');
const errorMessages = json.errors.map((e: any) => e.message).join('; ');
return {
success: false,
error: `GraphQL errors: ${errorMessages}`,
statusCode: response.status,
statusCode,
};
}

// Check if __schema is present
if (!json.data?.__schema) {
return {
success: false,
error: 'No __schema field in response. Introspection may be disabled on this endpoint.',
statusCode: response.status,
statusCode,
};
}

return {
success: true,
data: json.data,
statusCode: response.status,
statusCode,
};
} catch (err) {
clearTimeout(timeoutId);
Expand Down
21 changes: 11 additions & 10 deletions graphql/server/src/middleware/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const transformServiceToApi = (svc: Service): ApiStructure => {
const schemaNames =
api.apiExtensions?.nodes?.map((n: SchemaNode) => n.schemaName) || [];
const additionalSchemas =
api.schemasByApiSchemaApiIdAndSchemaId?.nodes?.map((n: SchemaNode) => n.schemaName) || [];
api.schemataByApiSchemaApiIdAndSchemaId?.nodes?.map((n: SchemaNode) => n.schemaName) || [];

let domains: string[] = [];
if (api.database?.sites?.nodes) {
Expand Down Expand Up @@ -161,6 +161,12 @@ const getHardCodedSchemata = ({
databaseId: string;
key: string;
}): any => {
const schemaNodes = schemata
.split(',')
.map((schema) => schema.trim())
.filter(Boolean)
.map((schemaName) => ({ schemaName }));

const svc = {
data: {
api: {
Expand All @@ -169,13 +175,8 @@ const getHardCodedSchemata = ({
dbname: opts.pg.database,
anonRole: 'administrator',
roleName: 'administrator',
schemaNamesFromExt: {
nodes: schemata
.split(',')
.map((schema) => schema.trim())
.map((schemaName) => ({ schemaName })),
},
schemaNames: { nodes: [] as Array<{ schemaName: string }> },
apiExtensions: { nodes: schemaNodes },
schemataByApiSchemaApiIdAndSchemaId: { nodes: [] as Array<{ schemaName: string }> },
apiModules: [] as Array<any>,
},
},
Expand Down Expand Up @@ -203,10 +204,10 @@ const getMetaSchema = ({
dbname: opts.pg.database,
anonRole: 'administrator',
roleName: 'administrator',
schemaNamesFromExt: {
apiExtensions: {
nodes: schemata.map((schemaName: string) => ({ schemaName })),
},
schemaNames: { nodes: [] as Array<{ schemaName: string }> },
schemataByApiSchemaApiIdAndSchemaId: { nodes: [] as Array<{ schemaName: string }> },
apiModules: [] as Array<any>,
},
},
Expand Down
2 changes: 1 addition & 1 deletion graphql/server/src/middleware/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const apiSelect = {
select: { schemaName: true },
connection: true,
},
schemasByApiSchemaApiIdAndSchemaId: {
schemataByApiSchemaApiIdAndSchemaId: {
select: { schemaName: true },
connection: true,
},
Expand Down
2 changes: 1 addition & 1 deletion graphql/server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export interface OldApiStructure {
dbname: string;
anonRole: string;
roleName: string;
schemasByApiSchemaApiIdAndSchemaId: SchemaNodes;
schemataByApiSchemaApiIdAndSchemaId: SchemaNodes;
apiExtensions: SchemaNodes;
apiModules: ApiModuleNodes;
rlsModule?: RlsModule;
Expand Down
17 changes: 16 additions & 1 deletion packages/cli/src/commands/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ Options:
--config <path> Path to graphql-codegen config file
--endpoint <url> GraphQL endpoint URL
--auth <token> Authorization header value (e.g., "Bearer 123")
--out <dir> Output directory (default: graphql/codegen/dist)
--header "Name: Value" Optional HTTP header; repeat to add multiple
--out <dir> Output directory
--dry-run Preview without writing files
-v, --verbose Verbose output

Expand Down Expand Up @@ -45,13 +46,27 @@ export default async (
const options: ConstructiveOptions = selectedDb ? getEnvOptions({ pg: { database: selectedDb } }) : getEnvOptions()
const schemasArg = (argv.schemas as string) || ''

// Parse repeatable --header args into a headers object
const headerArg = argv.header as string | string[] | undefined
const headerList = Array.isArray(headerArg) ? headerArg : headerArg ? [headerArg] : []
const headers: Record<string, string> = {}
for (const h of headerList) {
const idx = typeof h === 'string' ? h.indexOf(':') : -1
if (idx <= 0) continue
const name = h.slice(0, idx).trim()
const value = h.slice(idx + 1).trim()
if (!name) continue
headers[name] = value
}

const runGenerate = async ({ endpoint, schema }: { endpoint?: string; schema?: string }) => {
const result = await generateCommand({
config: configPath || undefined,
endpoint,
schema,
output: outDir,
authorization: auth || undefined,
headers,
verbose,
dryRun,
})
Expand Down