diff --git a/graphile/graphile-authz/package.json b/graphile/graphile-authz/package.json
index 7319d4ab0..1e185811a 100644
--- a/graphile/graphile-authz/package.json
+++ b/graphile/graphile-authz/package.json
@@ -25,7 +25,7 @@
"build": "makage build",
"build:dev": "makage build --dev",
"lint": "eslint . --fix",
- "test": "jest --passWithNoTests",
+ "test": "jest",
"test:watch": "jest --watch"
},
"devDependencies": {
diff --git a/graphile/graphile-query/package.json b/graphile/graphile-query/package.json
index 2a3c3675a..c03309d52 100644
--- a/graphile/graphile-query/package.json
+++ b/graphile/graphile-query/package.json
@@ -25,7 +25,7 @@
"build": "makage build",
"build:dev": "makage build --dev",
"lint": "eslint . --fix",
- "test": "jest --passWithNoTests",
+ "test": "jest",
"test:watch": "jest --watch"
},
"dependencies": {
diff --git a/graphile/graphile-schema/README.md b/graphile/graphile-schema/README.md
new file mode 100644
index 000000000..4049037ee
--- /dev/null
+++ b/graphile/graphile-schema/README.md
@@ -0,0 +1,97 @@
+# graphile-schema
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Lightweight GraphQL SDL builder for PostgreSQL using PostGraphile v5. Build schemas directly from a database or fetch them from a running GraphQL endpoint — no server dependencies required.
+
+## Installation
+
+```bash
+npm install graphile-schema
+```
+
+## Usage
+
+### Build SDL from a PostgreSQL Database
+
+```typescript
+import { buildSchemaSDL } from 'graphile-schema';
+
+const sdl = await buildSchemaSDL({
+ database: 'mydb',
+ schemas: ['app_public'],
+});
+
+console.log(sdl);
+```
+
+### Fetch SDL from a GraphQL Endpoint
+
+```typescript
+import { fetchEndpointSchemaSDL } from 'graphile-schema';
+
+const sdl = await fetchEndpointSchemaSDL('https://api.example.com/graphql', {
+ auth: 'Bearer my-token',
+ headers: { 'X-Custom-Header': 'value' },
+});
+
+console.log(sdl);
+```
+
+### With Custom Graphile Presets
+
+```typescript
+import { buildSchemaSDL } from 'graphile-schema';
+
+const sdl = await buildSchemaSDL({
+ database: 'mydb',
+ schemas: ['app_public', 'app_private'],
+ graphile: {
+ extends: [MyCustomPreset],
+ schema: { pgSimplifyPatch: false },
+ },
+});
+```
+
+## API
+
+### `buildSchemaSDL(opts)`
+
+Builds a GraphQL SDL string directly from a PostgreSQL database using PostGraphile v5 introspection.
+
+| Option | Type | Description |
+|--------|------|-------------|
+| `database` | `string` | Database name (default: `'constructive'`) |
+| `schemas` | `string[]` | PostgreSQL schemas to introspect |
+| `graphile` | `Partial` | Optional Graphile preset overrides |
+
+### `fetchEndpointSchemaSDL(endpoint, opts?)`
+
+Fetches a GraphQL SDL string from a running GraphQL endpoint via introspection query.
+
+| Option | Type | Description |
+|--------|------|-------------|
+| `endpoint` | `string` | GraphQL endpoint URL |
+| `opts.headerHost` | `string` | Override the `Host` header |
+| `opts.auth` | `string` | `Authorization` header value |
+| `opts.headers` | `Record` | Additional request headers |
+
+## Disclaimer
+
+AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
+
+No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
diff --git a/graphile/graphile-schema/__tests__/fetch-endpoint-schema.test.ts b/graphile/graphile-schema/__tests__/fetch-endpoint-schema.test.ts
new file mode 100644
index 000000000..c94afba81
--- /dev/null
+++ b/graphile/graphile-schema/__tests__/fetch-endpoint-schema.test.ts
@@ -0,0 +1,109 @@
+import * as http from 'node:http';
+import { getIntrospectionQuery, buildSchema, introspectionFromSchema } from 'graphql';
+
+import { fetchEndpointSchemaSDL } from '../src/fetch-endpoint-schema';
+
+const TEST_SDL = `
+ type Query {
+ hello: String
+ version: Int
+ }
+`;
+
+function createMockServer(handler: (req: http.IncomingMessage, res: http.ServerResponse) => void): Promise<{ server: http.Server; port: number }> {
+ return new Promise((resolve) => {
+ const server = http.createServer(handler);
+ server.listen(0, '127.0.0.1', () => {
+ const addr = server.address() as { port: number };
+ resolve({ server, port: addr.port });
+ });
+ });
+}
+
+function introspectionHandler(_req: http.IncomingMessage, res: http.ServerResponse) {
+ const schema = buildSchema(TEST_SDL);
+ const introspection = introspectionFromSchema(schema);
+ res.writeHead(200, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ data: introspection }));
+}
+
+describe('fetchEndpointSchemaSDL', () => {
+ let server: http.Server;
+ let port: number;
+
+ beforeAll(async () => {
+ ({ server, port } = await createMockServer(introspectionHandler));
+ });
+
+ afterAll(() => {
+ server.close();
+ });
+
+ it('fetches and returns SDL from a live endpoint', async () => {
+ const sdl = await fetchEndpointSchemaSDL(`http://127.0.0.1:${port}/graphql`);
+
+ expect(sdl).toContain('type Query');
+ expect(sdl).toContain('hello');
+ expect(sdl).toContain('version');
+ });
+
+ it('throws on HTTP error responses', async () => {
+ const { server: errServer, port: errPort } = await createMockServer((_req, res) => {
+ res.writeHead(500);
+ res.end('Internal Server Error');
+ });
+
+ await expect(
+ fetchEndpointSchemaSDL(`http://127.0.0.1:${errPort}/graphql`),
+ ).rejects.toThrow('HTTP 500');
+
+ errServer.close();
+ });
+
+ it('throws on invalid JSON response', async () => {
+ const { server: badServer, port: badPort } = await createMockServer((_req, res) => {
+ res.writeHead(200, { 'Content-Type': 'text/html' });
+ res.end('not json');
+ });
+
+ await expect(
+ fetchEndpointSchemaSDL(`http://127.0.0.1:${badPort}/graphql`),
+ ).rejects.toThrow('Failed to parse response');
+
+ badServer.close();
+ });
+
+ it('throws when introspection returns errors', async () => {
+ const { server: errServer, port: errPort } = await createMockServer((_req, res) => {
+ res.writeHead(200, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ errors: [{ message: 'Not allowed' }] }));
+ });
+
+ await expect(
+ fetchEndpointSchemaSDL(`http://127.0.0.1:${errPort}/graphql`),
+ ).rejects.toThrow('Introspection returned errors');
+
+ errServer.close();
+ });
+
+ it('passes custom headers to the endpoint', async () => {
+ let receivedHeaders: http.IncomingHttpHeaders = {};
+
+ const { server: headerServer, port: headerPort } = await createMockServer((req, res) => {
+ receivedHeaders = req.headers;
+ introspectionHandler(req, res);
+ });
+
+ await fetchEndpointSchemaSDL(`http://127.0.0.1:${headerPort}/graphql`, {
+ auth: 'Bearer test-token',
+ headerHost: 'custom.host.io',
+ headers: { 'X-Custom': 'value123' },
+ });
+
+ expect(receivedHeaders['authorization']).toBe('Bearer test-token');
+ expect(receivedHeaders['host']).toBe('custom.host.io');
+ expect(receivedHeaders['x-custom']).toBe('value123');
+
+ headerServer.close();
+ });
+});
diff --git a/graphile/graphile-schema/jest.config.js b/graphile/graphile-schema/jest.config.js
new file mode 100644
index 000000000..057a9420e
--- /dev/null
+++ b/graphile/graphile-schema/jest.config.js
@@ -0,0 +1,18 @@
+/** @type {import('ts-jest').JestConfigWithTsJest} */
+module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ transform: {
+ '^.+\\.tsx?$': [
+ 'ts-jest',
+ {
+ babelConfig: false,
+ tsconfig: 'tsconfig.json',
+ },
+ ],
+ },
+ transformIgnorePatterns: [`/node_modules/*`],
+ testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
+ modulePathIgnorePatterns: ['dist/*']
+};
diff --git a/graphile/graphile-schema/package.json b/graphile/graphile-schema/package.json
new file mode 100644
index 000000000..f9ab9b29c
--- /dev/null
+++ b/graphile/graphile-schema/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "graphile-schema",
+ "version": "1.0.0",
+ "author": "Constructive ",
+ "description": "Build GraphQL SDL from PostgreSQL databases using PostGraphile v5",
+ "main": "index.js",
+ "module": "esm/index.js",
+ "types": "index.d.ts",
+ "homepage": "https://github.com/constructive-io/constructive",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public",
+ "directory": "dist"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/constructive-io/constructive"
+ },
+ "bugs": {
+ "url": "https://github.com/constructive-io/constructive/issues"
+ },
+ "scripts": {
+ "clean": "makage clean",
+ "prepack": "npm run build",
+ "build": "makage build",
+ "build:dev": "makage build --dev",
+ "lint": "eslint . --fix",
+ "test": "jest",
+ "test:watch": "jest --watch"
+ },
+ "dependencies": {
+ "deepmerge": "^4.3.1",
+ "graphile-build": "^5.0.0-rc.3",
+ "graphile-config": "1.0.0-rc.3",
+ "graphile-settings": "workspace:^",
+ "graphql": "^16.9.0",
+ "pg-cache": "workspace:^",
+ "pg-env": "workspace:^"
+ },
+ "devDependencies": {
+ "makage": "^0.1.10",
+ "ts-node": "^10.9.2"
+ },
+ "keywords": [
+ "graphile",
+ "schema",
+ "graphql",
+ "sdl",
+ "postgraphile",
+ "introspection",
+ "constructive"
+ ]
+}
diff --git a/graphile/graphile-schema/src/build-schema.ts b/graphile/graphile-schema/src/build-schema.ts
new file mode 100644
index 000000000..34cf5b3ab
--- /dev/null
+++ b/graphile/graphile-schema/src/build-schema.ts
@@ -0,0 +1,44 @@
+import deepmerge from 'deepmerge'
+import { printSchema } from 'graphql'
+import { ConstructivePreset, makePgService } from 'graphile-settings'
+import { makeSchema } from 'graphile-build'
+import { buildConnectionString } from 'pg-cache'
+import { getPgEnvOptions } from 'pg-env'
+import type { GraphileConfig } from 'graphile-config'
+
+export type BuildSchemaOptions = {
+ database?: string;
+ schemas: string[];
+ graphile?: Partial;
+};
+
+export async function buildSchemaSDL(opts: BuildSchemaOptions): Promise {
+ const database = opts.database ?? 'constructive'
+ const schemas = Array.isArray(opts.schemas) ? opts.schemas : []
+
+ const config = getPgEnvOptions({ database })
+ const connectionString = buildConnectionString(
+ config.user,
+ config.password,
+ config.host,
+ config.port,
+ config.database,
+ )
+
+ const basePreset: GraphileConfig.Preset = {
+ extends: [ConstructivePreset],
+ pgServices: [
+ makePgService({
+ connectionString,
+ schemas,
+ }),
+ ],
+ }
+
+ const preset: GraphileConfig.Preset = opts.graphile
+ ? deepmerge(basePreset, opts.graphile)
+ : basePreset
+
+ const { schema } = await makeSchema(preset)
+ return printSchema(schema)
+}
diff --git a/graphql/server/src/schema.ts b/graphile/graphile-schema/src/fetch-endpoint-schema.ts
similarity index 52%
rename from graphql/server/src/schema.ts
rename to graphile/graphile-schema/src/fetch-endpoint-schema.ts
index 29daf8305..11a8d33b9 100644
--- a/graphql/server/src/schema.ts
+++ b/graphile/graphile-schema/src/fetch-endpoint-schema.ts
@@ -1,51 +1,14 @@
-import { printSchema, getIntrospectionQuery, buildClientSchema } from 'graphql'
-import { ConstructivePreset, makePgService } from 'graphile-settings'
-import { makeSchema } from 'graphile-build'
-import { getPgPool } from 'pg-cache'
-import type { GraphileConfig } from 'graphile-config'
+import { getIntrospectionQuery, buildClientSchema, printSchema } from 'graphql'
import * as http from 'node:http'
import * as https from 'node:https'
-export type BuildSchemaOptions = {
- database?: string;
- schemas: string[];
- graphile?: Partial;
+export type FetchEndpointSchemaOptions = {
+ headerHost?: string;
+ headers?: Record;
+ auth?: string;
};
-// Build GraphQL Schema SDL directly from Postgres using PostGraphile v5, without HTTP.
-export async function buildSchemaSDL(opts: BuildSchemaOptions): Promise {
- const database = opts.database ?? 'constructive'
- const schemas = Array.isArray(opts.schemas) ? opts.schemas : []
-
- // Get pool config for connection string
- const pool = getPgPool({ database })
- const poolConfig = (pool as any).options || {}
- const connectionString = `postgres://${poolConfig.user || 'postgres'}:${poolConfig.password || ''}@${poolConfig.host || 'localhost'}:${poolConfig.port || 5432}/${database}`
-
- // Build v5 preset
- const preset: GraphileConfig.Preset = {
- extends: [
- ConstructivePreset,
- ...(opts.graphile?.extends ?? []),
- ],
- ...(opts.graphile?.disablePlugins && { disablePlugins: opts.graphile.disablePlugins }),
- ...(opts.graphile?.plugins && { plugins: opts.graphile.plugins }),
- ...(opts.graphile?.schema && { schema: opts.graphile.schema }),
- pgServices: [
- makePgService({
- connectionString,
- schemas,
- }),
- ],
- }
-
- const { schema } = await makeSchema(preset)
- return printSchema(schema)
-}
-
-// Fetch GraphQL Schema SDL from a running GraphQL endpoint via introspection.
-// This centralizes GraphQL client usage in the server package to avoid duplicating deps in the CLI.
-export async function fetchEndpointSchemaSDL(endpoint: string, opts?: { headerHost?: string, headers?: Record, auth?: string }): Promise {
+export async function fetchEndpointSchemaSDL(endpoint: string, opts?: FetchEndpointSchemaOptions): Promise {
const url = new URL(endpoint)
const requestUrl = url
diff --git a/graphile/graphile-schema/src/index.ts b/graphile/graphile-schema/src/index.ts
new file mode 100644
index 000000000..840b76710
--- /dev/null
+++ b/graphile/graphile-schema/src/index.ts
@@ -0,0 +1,4 @@
+export { buildSchemaSDL } from './build-schema';
+export type { BuildSchemaOptions } from './build-schema';
+export { fetchEndpointSchemaSDL } from './fetch-endpoint-schema';
+export type { FetchEndpointSchemaOptions } from './fetch-endpoint-schema';
diff --git a/graphile/graphile-schema/tsconfig.esm.json b/graphile/graphile-schema/tsconfig.esm.json
new file mode 100644
index 000000000..d35ab5318
--- /dev/null
+++ b/graphile/graphile-schema/tsconfig.esm.json
@@ -0,0 +1,8 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist/esm",
+ "module": "ESNext",
+ "moduleResolution": "bundler"
+ }
+}
diff --git a/graphile/graphile-schema/tsconfig.json b/graphile/graphile-schema/tsconfig.json
new file mode 100644
index 000000000..a54950429
--- /dev/null
+++ b/graphile/graphile-schema/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src",
+ "moduleResolution": "nodenext",
+ "module": "nodenext"
+ },
+ "include": ["src/**/*"]
+}
diff --git a/graphile/graphile-search-plugin/package.json b/graphile/graphile-search-plugin/package.json
index f28b2ce92..a1ba1cb13 100644
--- a/graphile/graphile-search-plugin/package.json
+++ b/graphile/graphile-search-plugin/package.json
@@ -14,7 +14,7 @@
"build": "makage build",
"build:dev": "makage build --dev",
"lint": "eslint . --fix",
- "test": "jest --passWithNoTests",
+ "test": "jest",
"test:watch": "jest --watch"
},
"publishConfig": {
diff --git a/graphile/graphile-settings/package.json b/graphile/graphile-settings/package.json
index eed8207da..44a2f19ec 100644
--- a/graphile/graphile-settings/package.json
+++ b/graphile/graphile-settings/package.json
@@ -25,7 +25,7 @@
"build": "makage build",
"build:dev": "makage build --dev",
"lint": "eslint . --fix",
- "test": "jest --passWithNoTests",
+ "test": "jest",
"test:watch": "jest --watch"
},
"dependencies": {
diff --git a/graphile/graphile-test/package.json b/graphile/graphile-test/package.json
index 08a8698f0..498bc554b 100644
--- a/graphile/graphile-test/package.json
+++ b/graphile/graphile-test/package.json
@@ -25,7 +25,7 @@
"build": "makage build",
"build:dev": "makage build --dev",
"lint": "eslint . --fix",
- "test": "jest --passWithNoTests",
+ "test": "jest",
"test:watch": "jest --watch"
},
"devDependencies": {
diff --git a/graphile/postgraphile-plugin-pgvector/package.json b/graphile/postgraphile-plugin-pgvector/package.json
index 055ec5465..08fd62c69 100644
--- a/graphile/postgraphile-plugin-pgvector/package.json
+++ b/graphile/postgraphile-plugin-pgvector/package.json
@@ -25,7 +25,7 @@
"build": "makage build",
"build:dev": "makage build --dev",
"lint": "eslint . --fix",
- "test": "jest --passWithNoTests",
+ "test": "jest",
"test:watch": "jest --watch"
},
"devDependencies": {
diff --git a/graphql/codegen/package.json b/graphql/codegen/package.json
index d55395a78..bc067f506 100644
--- a/graphql/codegen/package.json
+++ b/graphql/codegen/package.json
@@ -42,7 +42,7 @@
"lint": "eslint . --fix",
"fmt": "oxfmt --write .",
"fmt:check": "oxfmt --check .",
- "test": "jest --passWithNoTests",
+ "test": "jest",
"test:watch": "jest --watch",
"example:codegen:sdk": "tsx src/cli/index.ts --config examples/multi-target.config.ts --react-query",
"example:codegen:orm": "tsx src/cli/index.ts --config examples/multi-target.config.ts --orm",
@@ -56,7 +56,6 @@
"@0no-co/graphql.web": "^1.1.2",
"@babel/generator": "^7.28.6",
"@babel/types": "^7.28.6",
- "@constructive-io/graphql-server": "workspace:^",
"@constructive-io/graphql-types": "workspace:^",
"@inquirerer/utils": "^3.2.3",
"@pgpmjs/core": "workspace:^",
@@ -64,6 +63,7 @@
"deepmerge": "^4.3.1",
"find-and-require-package-json": "^0.9.0",
"gql-ast": "workspace:^",
+ "graphile-schema": "workspace:^",
"graphql": "^16.9.0",
"inflekt": "^0.3.1",
"inquirerer": "^4.4.0",
diff --git a/graphql/codegen/src/__tests__/codegen/__snapshots__/cli-generator.test.ts.snap b/graphql/codegen/src/__tests__/codegen/__snapshots__/cli-generator.test.ts.snap
index cbdddfa8b..280608677 100644
--- a/graphql/codegen/src/__tests__/codegen/__snapshots__/cli-generator.test.ts.snap
+++ b/graphql/codegen/src/__tests__/codegen/__snapshots__/cli-generator.test.ts.snap
@@ -214,7 +214,10 @@ Common errors:
exports[`cli docs generator generates CLI README 1`] = `
"# myapp CLI
-> Auto-generated CLI commands from GraphQL schema
+
+
+
+
> @generated by @constructive-io/graphql-codegen - DO NOT EDIT
## Setup
@@ -345,6 +348,16 @@ All commands output JSON to stdout. Pipe to \`jq\` for formatting:
myapp car list | jq '.[]'
myapp car get --id | jq '.'
\`\`\`
+
+---
+
+Built by the [Constructive](https://constructive.io) team.
+
+## Disclaimer
+
+AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
+
+No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
"
`;
@@ -1604,7 +1617,10 @@ OUTPUT: UseMutationResult
exports[`hooks docs generator generates hooks README 1`] = `
"# React Query Hooks
-> Auto-generated React Query hooks from GraphQL schema
+
+
+
+
> @generated by @constructive-io/graphql-codegen - DO NOT EDIT
## Setup
@@ -1710,6 +1726,16 @@ Authenticate a user
|----------|------|
| \`email\` | String (required) |
| \`password\` | String (required) |
+
+---
+
+Built by the [Constructive](https://constructive.io) team.
+
+## Disclaimer
+
+AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
+
+No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
"
`;
@@ -2609,7 +2635,10 @@ exports[`multi-target cli docs generates multi-target MCP tools 1`] = `
exports[`multi-target cli docs generates multi-target README 1`] = `
"# myapp CLI
-> Auto-generated unified multi-target CLI from GraphQL schemas
+
+
+
+
> @generated by @constructive-io/graphql-codegen - DO NOT EDIT
## Setup
@@ -2819,6 +2848,16 @@ All commands output JSON to stdout. Pipe to \`jq\` for formatting:
myapp auth:user list | jq '.[]'
myapp auth:user get --id | jq '.'
\`\`\`
+
+---
+
+Built by the [Constructive](https://constructive.io) team.
+
+## Disclaimer
+
+AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
+
+No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
"
`;
@@ -4238,7 +4277,10 @@ const filtered = await db.modelName.findMany({ select: { id: true }, where: { na
exports[`orm docs generator generates ORM README 1`] = `
"# ORM Client
-> Auto-generated ORM client from GraphQL schema
+
+
+
+
> @generated by @constructive-io/graphql-codegen - DO NOT EDIT
## Setup
@@ -4354,6 +4396,16 @@ Authenticate a user
\`\`\`typescript
const result = await db.mutation.login({ email: '', password: '' }).execute();
\`\`\`
+
+---
+
+Built by the [Constructive](https://constructive.io) team.
+
+## Disclaimer
+
+AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
+
+No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
"
`;
@@ -5186,6 +5238,10 @@ exports[`target docs generator generates combined MCP config 1`] = `
exports[`target docs generator generates per-target README 1`] = `
"# Generated GraphQL SDK
+
+
+
+
> @generated by @constructive-io/graphql-codegen - DO NOT EDIT
## Overview
@@ -5230,12 +5286,26 @@ See [hooks/README.md](./hooks/README.md) for full hook reference.
inquirerer-based CLI commands for \`myapp\`.
See [cli/README.md](./cli/README.md) for command reference.
+
+---
+
+Built by the [Constructive](https://constructive.io) team.
+
+## Disclaimer
+
+AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
+
+No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
"
`;
exports[`target docs generator generates root-root README for multi-target 1`] = `
"# GraphQL SDK
+
+
+
+
> @generated by @constructive-io/graphql-codegen - DO NOT EDIT
## APIs
@@ -5244,5 +5314,15 @@ exports[`target docs generator generates root-root README for multi-target 1`] =
|-----|----------|------------|------|
| auth | http://auth.localhost/graphql | ORM | [./generated/auth/README.md](./generated/auth/README.md) |
| app | http://app.localhost/graphql | ORM, React Query, CLI | [./generated/app/README.md](./generated/app/README.md) |
+
+---
+
+Built by the [Constructive](https://constructive.io) team.
+
+## Disclaimer
+
+AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
+
+No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
"
`;
diff --git a/graphql/codegen/src/__tests__/codegen/expand-targets.test.ts b/graphql/codegen/src/__tests__/codegen/expand-targets.test.ts
new file mode 100644
index 000000000..1dc7ee33c
--- /dev/null
+++ b/graphql/codegen/src/__tests__/codegen/expand-targets.test.ts
@@ -0,0 +1,141 @@
+import * as fs from 'node:fs';
+import * as os from 'node:os';
+import * as path from 'node:path';
+
+import { expandApiNamesToMultiTarget, expandSchemaDirToMultiTarget } from '../../core/generate';
+
+describe('expandApiNamesToMultiTarget', () => {
+ it('returns null for no apiNames', () => {
+ expect(expandApiNamesToMultiTarget({})).toBeNull();
+ });
+
+ it('returns null for empty apiNames', () => {
+ expect(expandApiNamesToMultiTarget({ db: { apiNames: [] } })).toBeNull();
+ });
+
+ it('returns null for single apiName', () => {
+ expect(
+ expandApiNamesToMultiTarget({ db: { apiNames: ['app'] } }),
+ ).toBeNull();
+ });
+
+ it('expands multiple apiNames into separate targets', () => {
+ const result = expandApiNamesToMultiTarget({
+ db: { apiNames: ['app', 'admin'], pgpm: { workspacePath: '/ws', moduleName: 'mod' } },
+ output: './generated',
+ orm: true,
+ reactQuery: true,
+ });
+
+ expect(result).not.toBeNull();
+ expect(Object.keys(result!)).toEqual(['app', 'admin']);
+
+ expect(result!.app.db?.apiNames).toEqual(['app']);
+ expect(result!.app.output).toBe('./generated/app');
+ expect(result!.app.orm).toBe(true);
+ expect(result!.app.reactQuery).toBe(true);
+ expect(result!.app.db?.pgpm).toEqual({ workspacePath: '/ws', moduleName: 'mod' });
+
+ expect(result!.admin.db?.apiNames).toEqual(['admin']);
+ expect(result!.admin.output).toBe('./generated/admin');
+ });
+
+ it('uses default output path when output is not specified', () => {
+ const result = expandApiNamesToMultiTarget({
+ db: { apiNames: ['app', 'admin'] },
+ });
+
+ expect(result!.app.output).toBe('./generated/graphql/app');
+ expect(result!.admin.output).toBe('./generated/graphql/admin');
+ });
+
+ it('preserves all other config properties', () => {
+ const result = expandApiNamesToMultiTarget({
+ db: { apiNames: ['app', 'admin'], pgpm: { modulePath: '/mod' } },
+ endpoint: 'http://example.com',
+ tables: { include: ['users'] },
+ hooks: { queries: true },
+ });
+
+ expect(result!.app.endpoint).toBe('http://example.com');
+ expect(result!.app.tables).toEqual({ include: ['users'] });
+ expect(result!.app.hooks).toEqual({ queries: true });
+ });
+});
+
+describe('expandSchemaDirToMultiTarget', () => {
+ let tempDir: string;
+
+ beforeEach(() => {
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'expand-schema-dir-'));
+ });
+
+ afterEach(() => {
+ fs.rmSync(tempDir, { recursive: true, force: true });
+ });
+
+ it('returns null when schemaDir is not set', () => {
+ expect(expandSchemaDirToMultiTarget({})).toBeNull();
+ });
+
+ it('returns null when directory does not exist', () => {
+ expect(
+ expandSchemaDirToMultiTarget({ schemaDir: '/nonexistent/path' }),
+ ).toBeNull();
+ });
+
+ it('returns null when directory has no .graphql files', () => {
+ fs.writeFileSync(path.join(tempDir, 'readme.md'), 'hello');
+ expect(expandSchemaDirToMultiTarget({ schemaDir: tempDir })).toBeNull();
+ });
+
+ it('expands .graphql files into separate targets named by filename', () => {
+ fs.writeFileSync(path.join(tempDir, 'app.graphql'), 'type Query { hello: String }');
+ fs.writeFileSync(path.join(tempDir, 'admin.graphql'), 'type Query { users: [User] }');
+
+ const result = expandSchemaDirToMultiTarget({
+ schemaDir: tempDir,
+ output: './out',
+ orm: true,
+ });
+
+ expect(result).not.toBeNull();
+ expect(Object.keys(result!).sort()).toEqual(['admin', 'app']);
+
+ expect(result!.app.schemaFile).toBe(path.join(tempDir, 'app.graphql'));
+ expect(result!.app.output).toBe('./out/app');
+ expect(result!.app.orm).toBe(true);
+ expect(result!.app.schemaDir).toBeUndefined();
+
+ expect(result!.admin.schemaFile).toBe(path.join(tempDir, 'admin.graphql'));
+ expect(result!.admin.output).toBe('./out/admin');
+ });
+
+ it('uses default output path when output is not specified', () => {
+ fs.writeFileSync(path.join(tempDir, 'api.graphql'), 'type Query { ok: Boolean }');
+
+ const result = expandSchemaDirToMultiTarget({ schemaDir: tempDir });
+
+ expect(result!.api.output).toBe('./generated/graphql/api');
+ });
+
+ it('ignores non-.graphql files', () => {
+ fs.writeFileSync(path.join(tempDir, 'app.graphql'), 'type Query { a: String }');
+ fs.writeFileSync(path.join(tempDir, 'notes.txt'), 'not a schema');
+ fs.writeFileSync(path.join(tempDir, 'data.json'), '{}');
+
+ const result = expandSchemaDirToMultiTarget({ schemaDir: tempDir });
+
+ expect(Object.keys(result!)).toEqual(['app']);
+ });
+
+ it('sorts targets alphabetically', () => {
+ fs.writeFileSync(path.join(tempDir, 'zebra.graphql'), 'type Query { z: String }');
+ fs.writeFileSync(path.join(tempDir, 'alpha.graphql'), 'type Query { a: String }');
+ fs.writeFileSync(path.join(tempDir, 'mid.graphql'), 'type Query { m: String }');
+
+ const result = expandSchemaDirToMultiTarget({ schemaDir: tempDir });
+
+ expect(Object.keys(result!)).toEqual(['alpha', 'mid', 'zebra']);
+ });
+});
diff --git a/graphql/codegen/src/__tests__/codegen/format-output.test.ts b/graphql/codegen/src/__tests__/codegen/format-output.test.ts
deleted file mode 100644
index 35169f7fb..000000000
--- a/graphql/codegen/src/__tests__/codegen/format-output.test.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Test for formatOutput function
- * Verifies that oxfmt formats generated code correctly
- */
-import * as fs from 'node:fs';
-import * as os from 'node:os';
-import * as path from 'node:path';
-
-import { formatOutput } from '../../core/output';
-
-describe('formatOutput', () => {
- let tempDir: string;
-
- beforeEach(() => {
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegen-format-test-'));
- });
-
- afterEach(() => {
- fs.rmSync(tempDir, { recursive: true, force: true });
- });
-
- it('formats TypeScript files with oxfmt options', async () => {
- // Write unformatted code (double quotes, missing semicolons)
- const unformatted = `const x = "hello"
-const obj = {a: 1,b: 2}
-`;
- fs.writeFileSync(path.join(tempDir, 'test.ts'), unformatted);
-
- const result = await formatOutput(tempDir);
-
- // If oxfmt is not available in test environment, skip the test
- if (!result.success && result.error?.includes('oxfmt not available')) {
- console.log('Skipping test: oxfmt not available in test environment');
- return;
- }
-
- expect(result.success).toBe(true);
-
- // Verify formatting applied (single quotes, semicolons added)
- const formatted = fs.readFileSync(path.join(tempDir, 'test.ts'), 'utf-8');
- expect(formatted).toContain("'hello'");
- expect(formatted).toContain(';');
- });
-});
diff --git a/graphql/codegen/src/__tests__/codegen/schema-only.test.ts b/graphql/codegen/src/__tests__/codegen/schema-only.test.ts
new file mode 100644
index 000000000..7761e9a85
--- /dev/null
+++ b/graphql/codegen/src/__tests__/codegen/schema-only.test.ts
@@ -0,0 +1,87 @@
+import * as fs from 'node:fs';
+import * as os from 'node:os';
+import * as path from 'node:path';
+
+import { generate } from '../../core/generate';
+
+const EXAMPLE_SCHEMA = path.resolve(
+ __dirname,
+ '../../../examples/example.schema.graphql',
+);
+
+describe('generate() with schemaOnly', () => {
+ let tempDir: string;
+
+ beforeEach(() => {
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'schema-only-test-'));
+ });
+
+ afterEach(() => {
+ fs.rmSync(tempDir, { recursive: true, force: true });
+ });
+
+ it('writes SDL to file from schemaFile source', async () => {
+ const result = await generate({
+ schemaFile: EXAMPLE_SCHEMA,
+ schemaOnly: true,
+ schemaOnlyOutput: tempDir,
+ });
+
+ expect(result.success).toBe(true);
+ expect(result.filesWritten).toHaveLength(1);
+
+ const outFile = path.join(tempDir, 'schema.graphql');
+ expect(fs.existsSync(outFile)).toBe(true);
+
+ const sdl = fs.readFileSync(outFile, 'utf8');
+ expect(sdl).toContain('type Query');
+ expect(sdl).toContain('type User');
+ });
+
+ it('uses custom filename when schemaOnlyFilename is set', async () => {
+ const result = await generate({
+ schemaFile: EXAMPLE_SCHEMA,
+ schemaOnly: true,
+ schemaOnlyOutput: tempDir,
+ schemaOnlyFilename: 'app.graphql',
+ });
+
+ expect(result.success).toBe(true);
+ const outFile = path.join(tempDir, 'app.graphql');
+ expect(fs.existsSync(outFile)).toBe(true);
+ });
+
+ it('succeeds without any generators enabled', async () => {
+ const result = await generate({
+ schemaFile: EXAMPLE_SCHEMA,
+ schemaOnly: true,
+ schemaOnlyOutput: tempDir,
+ });
+
+ expect(result.success).toBe(true);
+ expect(result.message).toContain('Schema exported to');
+ });
+
+ it('fails when no source is specified', async () => {
+ const result = await generate({
+ schemaOnly: true,
+ schemaOnlyOutput: tempDir,
+ });
+
+ expect(result.success).toBe(false);
+ expect(result.message).toContain('No source specified');
+ });
+
+ it('creates output directory if it does not exist', async () => {
+ const nestedDir = path.join(tempDir, 'nested', 'output');
+
+ const result = await generate({
+ schemaFile: EXAMPLE_SCHEMA,
+ schemaOnly: true,
+ schemaOnlyOutput: nestedDir,
+ });
+
+ expect(result.success).toBe(true);
+ expect(fs.existsSync(path.join(nestedDir, 'schema.graphql'))).toBe(true);
+ });
+});
diff --git a/graphql/codegen/src/cli/handler.ts b/graphql/codegen/src/cli/handler.ts
new file mode 100644
index 000000000..110e5c259
--- /dev/null
+++ b/graphql/codegen/src/cli/handler.ts
@@ -0,0 +1,122 @@
+/**
+ * Shared codegen CLI handler
+ *
+ * Contains the core logic used by both `graphql-codegen` and `cnc codegen`.
+ * Both CLIs delegate to runCodegenHandler() after handling their own
+ * help/version flags.
+ */
+import type { Question } from 'inquirerer';
+
+import { findConfigFile, loadConfigFile } from '../core/config';
+import { expandApiNamesToMultiTarget, expandSchemaDirToMultiTarget, generate, generateMulti } from '../core/generate';
+import type { GraphQLSDKConfigTarget } from '../types/config';
+import {
+ buildDbConfig,
+ buildGenerateOptions,
+ camelizeArgv,
+ codegenQuestions,
+ hasResolvedCodegenSource,
+ normalizeCodegenListOptions,
+ printResult,
+ seedArgvFromConfig,
+} from './shared';
+
+interface Prompter {
+ prompt(argv: Record, questions: Question[]): Promise>;
+}
+
+export async function runCodegenHandler(
+ argv: Record,
+ prompter: Prompter,
+): Promise {
+ const args = camelizeArgv(argv as Record);
+
+ const schemaOnly = Boolean(args.schemaOnly);
+
+ const hasSourceFlags = Boolean(
+ args.endpoint || args.schemaFile || args.schemaDir || args.schemas || args.apiNames
+ );
+ const configPath =
+ (args.config as string | undefined) ||
+ (!hasSourceFlags ? findConfigFile() : undefined);
+ const targetName = args.target as string | undefined;
+
+ let fileConfig: GraphQLSDKConfigTarget = {};
+
+ if (configPath) {
+ const loaded = await loadConfigFile(configPath);
+ if (!loaded.success) {
+ console.error('x', loaded.error);
+ process.exit(1);
+ }
+
+ const config = loaded.config as Record;
+ const isMulti = !(
+ 'endpoint' in config ||
+ 'schemaFile' in config ||
+ 'schemaDir' in config ||
+ 'db' in config
+ );
+
+ if (isMulti) {
+ const targets = config as Record;
+
+ if (targetName && !targets[targetName]) {
+ console.error(
+ 'x',
+ `Target "${targetName}" not found. Available: ${Object.keys(targets).join(', ')}`,
+ );
+ process.exit(1);
+ }
+
+ const cliOptions = buildDbConfig(
+ normalizeCodegenListOptions(args),
+ );
+
+ const selectedTargets = targetName
+ ? { [targetName]: targets[targetName] }
+ : targets;
+
+ const { results, hasError } = await generateMulti({
+ configs: selectedTargets,
+ cliOverrides: cliOptions as Partial,
+ schemaOnly,
+ });
+
+ for (const { name, result } of results) {
+ console.log(`\n[${name}]`);
+ printResult(result);
+ }
+
+ if (hasError) process.exit(1);
+ return;
+ }
+
+ fileConfig = config as GraphQLSDKConfigTarget;
+ }
+
+ const seeded = seedArgvFromConfig(args, fileConfig);
+ const answers = hasResolvedCodegenSource(seeded)
+ ? seeded
+ : await prompter.prompt(seeded, codegenQuestions);
+ const options = buildGenerateOptions(answers, fileConfig);
+
+ const expandedApi = expandApiNamesToMultiTarget(options);
+ const expandedDir = expandSchemaDirToMultiTarget(options);
+ const expanded = expandedApi || expandedDir;
+ if (expanded) {
+ const { results, hasError } = await generateMulti({
+ configs: expanded,
+ schemaOnly,
+ });
+ for (const { name, result } of results) {
+ console.log(`\n[${name}]`);
+ printResult(result);
+ }
+ if (hasError) process.exit(1);
+ return;
+ }
+
+ const result = await generate({ ...options, schemaOnly });
+ printResult(result);
+}
diff --git a/graphql/codegen/src/cli/index.ts b/graphql/codegen/src/cli/index.ts
index dec25073c..6f448a6ed 100644
--- a/graphql/codegen/src/cli/index.ts
+++ b/graphql/codegen/src/cli/index.ts
@@ -7,19 +7,7 @@
*/
import { CLI, CLIOptions, getPackageJson, Inquirerer } from 'inquirerer';
-import { findConfigFile, loadConfigFile } from '../core/config';
-import { generate, generateMulti } from '../core/generate';
-import { mergeConfig, type GraphQLSDKConfigTarget } from '../types/config';
-import {
- buildDbConfig,
- buildGenerateOptions,
- camelizeArgv,
- codegenQuestions,
- hasResolvedCodegenSource,
- normalizeCodegenListOptions,
- printResult,
- seedArgvFromConfig,
-} from './shared';
+import { runCodegenHandler } from './handler';
const usage = `
graphql-codegen - GraphQL SDK generator for Constructive databases
@@ -31,10 +19,12 @@ Source Options (choose one):
-c, --config Path to config file
-e, --endpoint GraphQL endpoint URL
-s, --schema-file Path to GraphQL schema file
+ --schema-dir Directory of .graphql files (auto-expands to multi-target)
Database Options:
--schemas Comma-separated PostgreSQL schemas
--api-names Comma-separated API names (mutually exclusive with --schemas)
+ Multiple apiNames auto-expand to multi-target (one schema per API).
Generator Options:
--react-query Generate React Query hooks
@@ -45,6 +35,11 @@ Generator Options:
--dry-run Preview without writing files
-v, --verbose Show detailed output
+Schema Export:
+ --schema-only Export GraphQL SDL instead of running full codegen.
+ Works with any source (endpoint, file, database, PGPM).
+ With multiple apiNames, writes one .graphql per API.
+
-h, --help Show this help message
--version Show version number
`;
@@ -65,81 +60,7 @@ export const commands = async (
process.exit(0);
}
- const hasSourceFlags = Boolean(
- argv.endpoint ||
- argv.e ||
- argv['schema-file'] ||
- argv.s ||
- argv.schemas ||
- argv['api-names'],
- );
- const explicitConfigPath = (argv.config || argv.c) as string | undefined;
- const configPath =
- explicitConfigPath || (!hasSourceFlags ? findConfigFile() : undefined);
- const targetName = (argv.target || argv.t) as string | undefined;
-
- let fileConfig: GraphQLSDKConfigTarget = {};
-
- if (configPath) {
- const loaded = await loadConfigFile(configPath);
- if (!loaded.success) {
- console.error('x', loaded.error);
- process.exit(1);
- }
-
- const config = loaded.config as Record;
- const isMulti = !(
- 'endpoint' in config ||
- 'schemaFile' in config ||
- 'db' in config
- );
-
- if (isMulti) {
- const targets = config as Record;
-
- if (targetName && !targets[targetName]) {
- console.error(
- 'x',
- `Target "${targetName}" not found. Available: ${Object.keys(targets).join(', ')}`,
- );
- process.exit(1);
- }
-
- const cliOptions = buildDbConfig(
- normalizeCodegenListOptions(
- camelizeArgv(argv as Record),
- ),
- );
-
- const selectedTargets = targetName
- ? { [targetName]: targets[targetName] }
- : targets;
-
- const { results, hasError } = await generateMulti({
- configs: selectedTargets,
- cliOverrides: cliOptions as Partial,
- });
-
- for (const { name, result } of results) {
- console.log(`\n[${name}]`);
- printResult(result);
- }
-
- prompter.close();
- if (hasError) process.exit(1);
- return argv;
- }
-
- fileConfig = config as GraphQLSDKConfigTarget;
- }
-
- const seeded = seedArgvFromConfig(argv, fileConfig);
- const answers = hasResolvedCodegenSource(seeded)
- ? seeded
- : await prompter.prompt(seeded, codegenQuestions);
- const options = buildGenerateOptions(answers, fileConfig);
- const result = await generate(options);
- printResult(result);
+ await runCodegenHandler(argv, prompter);
prompter.close();
return argv;
};
@@ -156,16 +77,15 @@ export const options: Partial = {
a: 'authorization',
v: 'verbose',
},
+ boolean: ['schema-only'],
string: [
'config',
'endpoint',
'schema-file',
+ 'schema-dir',
'output',
'target',
'authorization',
- 'pgpm-module-path',
- 'pgpm-workspace-path',
- 'pgpm-module-name',
'schemas',
'api-names',
],
diff --git a/graphql/codegen/src/core/codegen/cli/docs-generator.ts b/graphql/codegen/src/core/codegen/cli/docs-generator.ts
index c039e6c18..88f063d37 100644
--- a/graphql/codegen/src/core/codegen/cli/docs-generator.ts
+++ b/graphql/codegen/src/core/codegen/cli/docs-generator.ts
@@ -4,6 +4,8 @@ import type { CleanTable, CleanOperation } from '../../../types/schema';
import {
formatArgType,
getEditableFields,
+ getReadmeHeader,
+ getReadmeFooter,
gqlTypeToJsonSchemaType,
buildSkillFile,
} from '../docs-utils';
@@ -24,11 +26,7 @@ export function generateReadme(
): GeneratedDocFile {
const lines: string[] = [];
- lines.push(`# ${toolName} CLI`);
- lines.push('');
- lines.push('> Auto-generated CLI commands from GraphQL schema');
- lines.push('> @generated by @constructive-io/graphql-codegen - DO NOT EDIT');
- lines.push('');
+ lines.push(...getReadmeHeader(`${toolName} CLI`));
lines.push('## Setup');
lines.push('');
lines.push('```bash');
@@ -157,6 +155,8 @@ export function generateReadme(
lines.push('```');
lines.push('');
+ lines.push(...getReadmeFooter());
+
return {
fileName: 'README.md',
content: lines.join('\n'),
@@ -737,11 +737,7 @@ export function generateMultiTargetReadme(
const { toolName, builtinNames, targets } = input;
const lines: string[] = [];
- lines.push(`# ${toolName} CLI`);
- lines.push('');
- lines.push('> Auto-generated unified multi-target CLI from GraphQL schemas');
- lines.push('> @generated by @constructive-io/graphql-codegen - DO NOT EDIT');
- lines.push('');
+ lines.push(...getReadmeHeader(`${toolName} CLI`));
lines.push('## Setup');
lines.push('');
@@ -938,6 +934,8 @@ export function generateMultiTargetReadme(
lines.push('```');
lines.push('');
+ lines.push(...getReadmeFooter());
+
return {
fileName: 'README.md',
content: lines.join('\n'),
diff --git a/graphql/codegen/src/core/codegen/docs-utils.ts b/graphql/codegen/src/core/codegen/docs-utils.ts
index 2e5a7d4b6..bab5c1d74 100644
--- a/graphql/codegen/src/core/codegen/docs-utils.ts
+++ b/graphql/codegen/src/core/codegen/docs-utils.ts
@@ -22,6 +22,39 @@ export interface SkillDefinition {
language?: string;
}
+const CONSTRUCTIVE_LOGO_URL =
+ 'https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg';
+
+const CONSTRUCTIVE_REPO = 'https://github.com/constructive-io/constructive';
+
+export function getReadmeHeader(title: string): string[] {
+ return [
+ `# ${title}`,
+ '',
+ '',
+ `
`,
+ '
',
+ '',
+ `> @generated by @constructive-io/graphql-codegen - DO NOT EDIT`,
+ '',
+ ];
+}
+
+export function getReadmeFooter(): string[] {
+ return [
+ '---',
+ '',
+ 'Built by the [Constructive](https://constructive.io) team.',
+ '',
+ '## Disclaimer',
+ '',
+ 'AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.',
+ '',
+ 'No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.',
+ '',
+ ];
+}
+
export function resolveDocsConfig(
docs: DocsConfig | boolean | undefined,
): DocsConfig {
diff --git a/graphql/codegen/src/core/codegen/hooks-docs-generator.ts b/graphql/codegen/src/core/codegen/hooks-docs-generator.ts
index 12e6ffd5a..4a7f93e45 100644
--- a/graphql/codegen/src/core/codegen/hooks-docs-generator.ts
+++ b/graphql/codegen/src/core/codegen/hooks-docs-generator.ts
@@ -2,6 +2,8 @@ import type { CleanOperation, CleanTable } from '../../types/schema';
import {
buildSkillFile,
formatArgType,
+ getReadmeHeader,
+ getReadmeFooter,
gqlTypeToJsonSchemaType,
} from './docs-utils';
import type { GeneratedDocFile, McpTool } from './docs-utils';
@@ -33,11 +35,7 @@ export function generateHooksReadme(
): GeneratedDocFile {
const lines: string[] = [];
- lines.push('# React Query Hooks');
- lines.push('');
- lines.push('> Auto-generated React Query hooks from GraphQL schema');
- lines.push('> @generated by @constructive-io/graphql-codegen - DO NOT EDIT');
- lines.push('');
+ lines.push(...getReadmeHeader('React Query Hooks'));
lines.push('## Setup');
lines.push('');
lines.push('```typescript');
@@ -170,6 +168,8 @@ export function generateHooksReadme(
}
}
+ lines.push(...getReadmeFooter());
+
return {
fileName: 'README.md',
content: lines.join('\n'),
diff --git a/graphql/codegen/src/core/codegen/orm/docs-generator.ts b/graphql/codegen/src/core/codegen/orm/docs-generator.ts
index 1389d6160..950994c60 100644
--- a/graphql/codegen/src/core/codegen/orm/docs-generator.ts
+++ b/graphql/codegen/src/core/codegen/orm/docs-generator.ts
@@ -3,6 +3,8 @@ import {
buildSkillFile,
formatArgType,
getEditableFields,
+ getReadmeHeader,
+ getReadmeFooter,
gqlTypeToJsonSchemaType,
} from '../docs-utils';
import type { GeneratedDocFile, McpTool } from '../docs-utils';
@@ -20,11 +22,7 @@ export function generateOrmReadme(
): GeneratedDocFile {
const lines: string[] = [];
- lines.push('# ORM Client');
- lines.push('');
- lines.push('> Auto-generated ORM client from GraphQL schema');
- lines.push('> @generated by @constructive-io/graphql-codegen - DO NOT EDIT');
- lines.push('');
+ lines.push(...getReadmeHeader('ORM Client'));
lines.push('## Setup');
lines.push('');
lines.push('```typescript');
@@ -143,6 +141,8 @@ export function generateOrmReadme(
}
}
+ lines.push(...getReadmeFooter());
+
return {
fileName: 'README.md',
content: lines.join('\n'),
diff --git a/graphql/codegen/src/core/codegen/target-docs-generator.ts b/graphql/codegen/src/core/codegen/target-docs-generator.ts
index ab8b5d851..d5215b77d 100644
--- a/graphql/codegen/src/core/codegen/target-docs-generator.ts
+++ b/graphql/codegen/src/core/codegen/target-docs-generator.ts
@@ -1,4 +1,5 @@
import type { GraphQLSDKConfigTarget } from '../../types/config';
+import { getReadmeHeader, getReadmeFooter } from './docs-utils';
import type { GeneratedDocFile, McpTool } from './docs-utils';
export interface TargetReadmeOptions {
@@ -25,10 +26,7 @@ export function generateTargetReadme(
} = options;
const lines: string[] = [];
- lines.push('# Generated GraphQL SDK');
- lines.push('');
- lines.push('> @generated by @constructive-io/graphql-codegen - DO NOT EDIT');
- lines.push('');
+ lines.push(...getReadmeHeader('Generated GraphQL SDK'));
lines.push('## Overview');
lines.push('');
@@ -110,6 +108,8 @@ export function generateTargetReadme(
lines.push('');
}
+ lines.push(...getReadmeFooter());
+
return {
fileName: 'README.md',
content: lines.join('\n'),
@@ -145,10 +145,7 @@ export function generateRootRootReadme(
): GeneratedDocFile {
const lines: string[] = [];
- lines.push('# GraphQL SDK');
- lines.push('');
- lines.push('> @generated by @constructive-io/graphql-codegen - DO NOT EDIT');
- lines.push('');
+ lines.push(...getReadmeHeader('GraphQL SDK'));
lines.push('## APIs');
lines.push('');
@@ -163,6 +160,8 @@ export function generateRootRootReadme(
}
lines.push('');
+ lines.push(...getReadmeFooter());
+
return {
fileName: 'README.md',
content: lines.join('\n'),
diff --git a/graphql/codegen/src/core/database/index.ts b/graphql/codegen/src/core/database/index.ts
index 1b6cfae53..b078602f8 100644
--- a/graphql/codegen/src/core/database/index.ts
+++ b/graphql/codegen/src/core/database/index.ts
@@ -7,7 +7,7 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
-import { buildSchemaSDL } from '@constructive-io/graphql-server';
+import { buildSchemaSDL } from 'graphile-schema';
export interface BuildSchemaFromDatabaseOptions {
/** Database name */
@@ -56,24 +56,3 @@ export async function buildSchemaFromDatabase(
return { schemaPath, sdl };
}
-
-/**
- * Build a GraphQL schema SDL string from a PostgreSQL database without writing to file.
- *
- * This is a convenience wrapper around buildSchemaSDL from graphql-server.
- *
- * @param options - Configuration options
- * @returns The SDL content as a string
- */
-export async function buildSchemaSDLFromDatabase(options: {
- database: string;
- schemas: string[];
-}): Promise {
- const { database, schemas } = options;
-
- // PostGraphile v5 resolves role/settings via preset configuration.
- return buildSchemaSDL({
- database,
- schemas,
- });
-}
diff --git a/graphql/codegen/src/core/generate.ts b/graphql/codegen/src/core/generate.ts
index 3dccf5b22..034eb1ee2 100644
--- a/graphql/codegen/src/core/generate.ts
+++ b/graphql/codegen/src/core/generate.ts
@@ -4,9 +4,16 @@
* This is the primary entry point for programmatic usage.
* The CLI is a thin wrapper around this function.
*/
+import * as fs from 'node:fs';
import path from 'node:path';
-import type { CliConfig, GraphQLSDKConfigTarget } from '../types/config';
+import { buildClientSchema, printSchema } from 'graphql';
+
+import { PgpmPackage } from '@pgpmjs/core';
+import { createEphemeralDb, type EphemeralDbResult } from 'pgsql-client';
+import { deployPgpm } from 'pgsql-seed';
+
+import type { CliConfig, DbConfig, GraphQLSDKConfigTarget, PgpmConfig } from '../types/config';
import { getConfigOptions } from '../types/config';
import type { CleanOperation, CleanTable } from '../types/schema';
import { generate as generateReactQueryFiles } from './codegen';
@@ -55,6 +62,9 @@ export interface GenerateOptions extends GraphQLSDKConfigTarget {
verbose?: boolean;
dryRun?: boolean;
skipCustomOperations?: boolean;
+ schemaOnly?: boolean;
+ schemaOnlyOutput?: string;
+ schemaOnlyFilename?: string;
}
export interface GenerateResult {
@@ -99,7 +109,7 @@ export async function generate(
const runOrm =
runReactQuery || !!config.cli || (options.orm !== undefined ? !!options.orm : false);
- if (!runReactQuery && !runOrm && !runCli) {
+ if (!options.schemaOnly && !runReactQuery && !runOrm && !runCli) {
return {
success: false,
message:
@@ -130,6 +140,42 @@ export async function generate(
headers: config.headers,
});
+ if (options.schemaOnly) {
+ try {
+ console.log(`Fetching schema from ${source.describe()}...`);
+ const { introspection } = await source.fetch();
+ const schema = buildClientSchema(introspection as any);
+ const sdl = printSchema(schema);
+
+ if (!sdl.trim()) {
+ return {
+ success: false,
+ message: 'Schema introspection returned empty SDL.',
+ output: outputRoot,
+ };
+ }
+
+ const outDir = path.resolve(options.schemaOnlyOutput || outputRoot || '.');
+ await fs.promises.mkdir(outDir, { recursive: true });
+ const filename = options.schemaOnlyFilename || 'schema.graphql';
+ const filePath = path.join(outDir, filename);
+ await fs.promises.writeFile(filePath, sdl, 'utf-8');
+
+ return {
+ success: true,
+ message: `Schema exported to ${filePath}`,
+ output: outDir,
+ filesWritten: [filePath],
+ };
+ } catch (err) {
+ return {
+ success: false,
+ message: `Failed to export schema: ${err instanceof Error ? err.message : 'Unknown error'}`,
+ output: outputRoot,
+ };
+ }
+ }
+
// Run pipeline
let pipelineResult: Awaited>;
try {
@@ -383,11 +429,66 @@ export async function generate(
};
}
+export function expandApiNamesToMultiTarget(
+ config: GraphQLSDKConfigTarget,
+): Record | null {
+ const apiNames = config.db?.apiNames;
+ if (!apiNames || apiNames.length <= 1) return null;
+
+ const targets: Record = {};
+ for (const apiName of apiNames) {
+ targets[apiName] = {
+ ...config,
+ db: {
+ ...config.db,
+ apiNames: [apiName],
+ },
+ output: config.output
+ ? `${config.output}/${apiName}`
+ : `./generated/graphql/${apiName}`,
+ };
+ }
+ return targets;
+}
+
+export function expandSchemaDirToMultiTarget(
+ config: GraphQLSDKConfigTarget,
+): Record | null {
+ const schemaDir = config.schemaDir;
+ if (!schemaDir) return null;
+
+ const resolvedDir = path.resolve(schemaDir);
+ if (!fs.existsSync(resolvedDir) || !fs.statSync(resolvedDir).isDirectory()) {
+ return null;
+ }
+
+ const graphqlFiles = fs.readdirSync(resolvedDir)
+ .filter((f) => f.endsWith('.graphql'))
+ .sort();
+
+ if (graphqlFiles.length === 0) return null;
+
+ const targets: Record = {};
+ for (const file of graphqlFiles) {
+ const name = path.basename(file, '.graphql');
+ targets[name] = {
+ ...config,
+ schemaDir: undefined,
+ schemaFile: path.join(resolvedDir, file),
+ output: config.output
+ ? `${config.output}/${name}`
+ : `./generated/graphql/${name}`,
+ };
+ }
+ return targets;
+}
+
export interface GenerateMultiOptions {
configs: Record;
cliOverrides?: Partial;
verbose?: boolean;
dryRun?: boolean;
+ schemaOnly?: boolean;
unifiedCli?: CliConfig | boolean;
}
@@ -396,26 +497,143 @@ export interface GenerateMultiResult {
hasError: boolean;
}
+interface SharedPgpmSource {
+ key: string;
+ ephemeralDb: EphemeralDbResult;
+ deployed: boolean;
+}
+
+function getPgpmSourceKey(pgpm: PgpmConfig): string | null {
+ if (pgpm.modulePath) return `module:${path.resolve(pgpm.modulePath)}`;
+ if (pgpm.workspacePath && pgpm.moduleName)
+ return `workspace:${path.resolve(pgpm.workspacePath)}:${pgpm.moduleName}`;
+ return null;
+}
+
+function getModulePathFromPgpm(
+ pgpm: PgpmConfig,
+): string {
+ if (pgpm.modulePath) return pgpm.modulePath;
+ if (pgpm.workspacePath && pgpm.moduleName) {
+ const workspace = new PgpmPackage(pgpm.workspacePath);
+ const moduleProject = workspace.getModuleProject(pgpm.moduleName);
+ const modulePath = moduleProject.getModulePath();
+ if (!modulePath) {
+ throw new Error(`Module "${pgpm.moduleName}" not found in workspace`);
+ }
+ return modulePath;
+ }
+ throw new Error('Invalid PGPM config: requires modulePath or workspacePath+moduleName');
+}
+
+async function prepareSharedPgpmSources(
+ configs: Record,
+ cliOverrides?: Partial,
+): Promise