Skip to content
Merged
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
57 changes: 14 additions & 43 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,55 +58,26 @@ cnc explorer --origin http://localhost:3000

### `cnc codegen`

Generate TypeScript types, operations, and SDK from a GraphQL schema.
Generate TypeScript types, operations, and SDK from a GraphQL schema or endpoint.

```bash
# From SDL file
cnc codegen --schema ./schema.graphql --out ./codegen
# From endpoint
cnc codegen --endpoint http://localhost:5555/graphql --out ./codegen

# From endpoint with Host override
cnc codegen --endpoint http://localhost:3000/graphql --headerHost meta8.localhost --out ./codegen
# From database
cnc codegen --database constructive_db --out ./codegen --verbose
```

**Options:**

- `--schema <path>` - Schema SDL file path
- `--endpoint <url>` - GraphQL endpoint to fetch schema via introspection
- `--headerHost <host>` - Optional Host header for endpoint requests
- `--auth <token>` - Optional Authorization header value
- `--header "Name: Value"` - Optional HTTP header (repeatable)
- `--out <dir>` - Output root directory (default: graphql/codegen/dist)
- `--format <gql|ts>` - Document format (default: gql)
- `--convention <style>` - Filename convention (dashed|underscore|camelcase|camelUpper)
- `--emitTypes <bool>` - Emit types (default: true)
- `--emitOperations <bool>` - Emit operations (default: true)
- `--emitSdk <bool>` - Emit SDK (default: true)
- `--config <path>` - Config file (JSON/YAML)

**Config file example (`codegen.json`):**

```json
{
"input": {
"schema": "./schema.graphql",
"headers": { "Host": "meta8.localhost" }
},
"output": {
"root": "graphql/codegen/dist"
},
"documents": {
"format": "gql",
"convention": "dashed",
"excludePatterns": [".*Module$"]
},
"features": {
"emitTypes": true,
"emitOperations": true,
"emitSdk": true,
"emitReactQuery": true
}
}
```
- `--endpoint <url>` - GraphQL endpoint URL
- `--auth <token>` - Authorization header value (e.g., "Bearer 123")
- `--out <dir>` - Output directory (default: graphql/codegen/dist)
- `--dry-run` - Preview without writing files
- `--verbose` - Verbose output

- `--database <name>` - Database override for DB mode (defaults to PGDATABASE)
- `--schemas <list>` - Comma-separated schemas (required unless using --endpoint)

### `cnc get-graphql-schema`

Expand All @@ -123,7 +94,7 @@ cnc get-graphql-schema --endpoint http://localhost:3000/graphql --out ./schema.g
**Options:**

- `--database <name>` - Database name (for programmatic builder)
- `--schemas <list>` - Comma-separated schemas to include
- `--schemas <list>` - Comma-separated schemas to include (required unless using --endpoint)
- `--endpoint <url>` - GraphQL endpoint to fetch schema via introspection
- `--headerHost <host>` - Optional Host header for endpoint requests
- `--auth <token>` - Optional Authorization header value
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ jest.mock('@constructive-io/graphql-codegen/cli/commands/generate', () => ({
generateCommand: async () => ({ success: true, message: 'ok' })
}));
import { Inquirerer, Question } from 'inquirerer';
jest.mock('@constructive-io/graphql-codegen/cli/commands/generate', () => ({
generateCommand: jest.fn(async () => ({ success: true, message: 'Generated SDK', filesWritten: [] as string[] }))
}))

import { KEY_SEQUENCES, setupTests, TestEnvironment } from '../test-utils';

Expand Down
63 changes: 27 additions & 36 deletions packages/cli/__tests__/codegen.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import type { ParsedArgs } from 'minimist'
import codegenCommand from '../src/commands/codegen'
import { generateCommand } from '@constructive-io/graphql-codegen/cli/commands/generate'

const generateMock = jest.fn(async (_opts?: any) => ({ success: true, message: 'ok' }))
jest.mock('@constructive-io/graphql-codegen/cli/commands/generate', () => ({
generateCommand: (opts: any) => generateMock(opts)
generateCommand: jest.fn(async () => ({ success: true, message: 'Generated SDK', filesWritten: [] as string[] }))
}))

jest.mock('@constructive-io/graphql-server', () => ({
buildSchemaSDL: jest.fn(async () => 'type Query { hello: String }\nschema { query: Query }')
}))

describe('codegen command', () => {
Expand All @@ -26,56 +30,43 @@ describe('codegen command', () => {
spyExit.mockRestore()
})

it('passes endpoint, out, auth, and flags to generateCommand', async () => {
it('calls generateCommand with endpoint flow options', async () => {

const argv: Partial<ParsedArgs> = {
endpoint: 'http://localhost:3000/graphql',
auth: 'Bearer testtoken',
out: 'graphql/codegen/dist',
v: true,
verbose: true,
'dry-run': true
}

await codegenCommand(argv, {} as any, {} as any)

expect(generateMock).toHaveBeenCalled()
const opts = (generateMock as jest.Mock).mock.calls[0][0]
expect(opts.endpoint).toBe('http://localhost:3000/graphql')
expect(opts.output).toBe('graphql/codegen/dist')
expect(opts.authorization).toBe('Bearer testtoken')
expect(opts.verbose).toBe(true)
expect(opts.dryRun).toBe(true)
expect(generateCommand).toHaveBeenCalled()
const call = (generateCommand as jest.Mock).mock.calls[0][0]
expect(call).toMatchObject({
endpoint: 'http://localhost:3000/graphql',
output: 'graphql/codegen/dist',
authorization: 'Bearer testtoken',
verbose: true,
dryRun: true
})
})

it('passes config path and out directory to generateCommand', async () => {
it('builds schema file and calls generateCommand with schema when DB options provided', async () => {

const argv: Partial<ParsedArgs> = {
config: '/tmp/codegen.json',
database: 'constructive_db',
schemas: 'public',
out: 'graphql/codegen/dist'
}

await codegenCommand(argv, {} as any, {} as any)

const opts = (generateMock as jest.Mock).mock.calls[0][0]
expect(opts.config).toBe('/tmp/codegen.json')
expect(opts.output).toBe('graphql/codegen/dist')
})

it('does not depend on process.env.CONSTRUCTIVE_CODEGEN_BIN', async () => {
delete process.env.CONSTRUCTIVE_CODEGEN_BIN
const argv: Partial<ParsedArgs> = { out: 'graphql/codegen/dist' }
await codegenCommand(argv, {} as any, {} as any)
expect(generateMock).toHaveBeenCalled()
})

it('exits with non-zero when generateCommand fails', async () => {
generateMock.mockResolvedValueOnce({ success: false, message: 'fail', errors: ['e1'] } as any)
const spyExit = jest.spyOn(process, 'exit').mockImplementation(((code?: number) => { throw new Error('exit:' + code) }) as any)

const argv: Partial<ParsedArgs> = {
endpoint: 'http://localhost:3000/graphql',
out: 'graphql/codegen/dist'
}

await expect(codegenCommand(argv, {} as any, {} as any)).rejects.toThrow('exit:1')
spyExit.mockRestore()
expect(generateCommand).toHaveBeenCalled()
const call = (generateCommand as jest.Mock).mock.calls[0][0]
expect(call.schema).toBe('graphql/codegen/dist/schema.graphql')
expect(call.output).toBe('graphql/codegen/dist')
expect(call.endpoint).toBeUndefined()
})
})
2 changes: 1 addition & 1 deletion packages/cli/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const createCommandMap = (): Record<string, Function> => {
server,
explorer,
'get-graphql-schema': getGraphqlSchema,
codegen
codegen,
};
};

Expand Down
68 changes: 56 additions & 12 deletions packages/cli/src/commands/codegen.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { CLIOptions, Inquirerer } from 'inquirerer'
import { ParsedArgs } from 'minimist'
import * as fs from 'node:fs'
import * as path from 'node:path'
import { buildSchemaSDL } from '@constructive-io/graphql-server'
import { generateCommand } from '@constructive-io/graphql-codegen/cli/commands/generate'
import { ConstructiveOptions, getEnvOptions } from '@constructive-io/graphql-env'

const usage = `
Constructive GraphQL Codegen:
Expand All @@ -15,6 +19,9 @@ Options:
--out <dir> Output directory (default: graphql/codegen/dist)
--dry-run Preview without writing files
-v, --verbose Verbose output

--database <name> Database override for DB mode (defaults to PGDATABASE)
--schemas <list> Comma-separated schemas (required for DB mode)
`

export default async (
Expand All @@ -27,24 +34,61 @@ export default async (
process.exit(0)
}

const endpoint = (argv.endpoint as string) || ''
const endpointArg = (argv.endpoint as string) || ''
const outDir = (argv.out as string) || 'codegen'
const auth = (argv.auth as string) || ''
const configPath = (argv.config as string) || ''
const dryRun = !!(argv['dry-run'] || argv.dryRun)
const verbose = !!(argv.verbose || argv.v)

const result = await generateCommand({
config: configPath,
endpoint,
output: outDir,
authorization: auth,
verbose,
dryRun,
})
if (!result.success) {
console.error('x', result.message)
const selectedDb = (argv.database as string) || undefined
const options: ConstructiveOptions = selectedDb ? getEnvOptions({ pg: { database: selectedDb } }) : getEnvOptions()
const schemasArg = (argv.schemas as string) || ''

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

if (!result.success) {
console.error(result.message)
if (result.errors?.length) result.errors.forEach(e => console.error(' -', e))
process.exit(1)
}
console.log(result.message)
if (result.filesWritten?.length) {
result.filesWritten.forEach(f => console.log(f))
}
}

if (endpointArg) {
await runGenerate({ endpoint: endpointArg })
return
}

if (!schemasArg.trim()) {
console.error('Error: --schemas is required when building from database. Provide a comma-separated list of schemas.')
process.exit(1)
}
console.log('[ok]', result.message)

const schemas = schemasArg.split(',').map((s: string) => s.trim()).filter(Boolean)
await fs.promises.mkdir(outDir, { recursive: true })
const sdl = await buildSchemaSDL({
database: options.pg.database,
schemas,
graphile: {
pgSettings: async () => ({ role: 'administrator' }),
},
})

const schemaPath = path.join(outDir, 'schema.graphql')
await fs.promises.writeFile(schemaPath, sdl, 'utf-8')

await runGenerate({ schema: schemaPath })
}
Loading