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
2 changes: 1 addition & 1 deletion graphile/graphile-authz/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion graphile/graphile-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
97 changes: 97 additions & 0 deletions graphile/graphile-schema/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# graphile-schema

<p align="center" width="100%">
<img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
</p>

<p align="center" width="100%">
<a href="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml">
<img height="20" src="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml/badge.svg" />
</a>
<a href="https://github.com/constructive-io/constructive/blob/main/LICENSE">
<img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/>
</a>
<a href="https://www.npmjs.com/package/graphile-schema">
<img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=graphile%2Fgraphile-schema%2Fpackage.json"/>
</a>
</p>

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<GraphileConfig.Preset>` | 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<string, string>` | 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.
109 changes: 109 additions & 0 deletions graphile/graphile-schema/__tests__/fetch-endpoint-schema.test.ts
Original file line number Diff line number Diff line change
@@ -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('<html>not json</html>');
});

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();
});
});
18 changes: 18 additions & 0 deletions graphile/graphile-schema/jest.config.js
Original file line number Diff line number Diff line change
@@ -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/*']
};
53 changes: 53 additions & 0 deletions graphile/graphile-schema/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "graphile-schema",
"version": "1.0.0",
"author": "Constructive <developers@constructive.io>",
"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"
]
}
44 changes: 44 additions & 0 deletions graphile/graphile-schema/src/build-schema.ts
Original file line number Diff line number Diff line change
@@ -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<GraphileConfig.Preset>;
};

export async function buildSchemaSDL(opts: BuildSchemaOptions): Promise<string> {
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)
}
Loading