Skip to content

Commit 7ae4636

Browse files
authored
Merge pull request #731 from constructive-io/devin/1771408168-pgpm-multi-target-improvements
feat(codegen): apiNames auto-expand, schemaDir, --schema-only, graphile-schema package, README headers
2 parents 44410af + c61e917 commit 7ae4636

95 files changed

Lines changed: 18734 additions & 8915 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

graphile/graphile-authz/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"build": "makage build",
2626
"build:dev": "makage build --dev",
2727
"lint": "eslint . --fix",
28-
"test": "jest --passWithNoTests",
28+
"test": "jest",
2929
"test:watch": "jest --watch"
3030
},
3131
"devDependencies": {

graphile/graphile-query/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"build": "makage build",
2626
"build:dev": "makage build --dev",
2727
"lint": "eslint . --fix",
28-
"test": "jest --passWithNoTests",
28+
"test": "jest",
2929
"test:watch": "jest --watch"
3030
},
3131
"dependencies": {

graphile/graphile-schema/README.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# graphile-schema
2+
3+
<p align="center" width="100%">
4+
<img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
5+
</p>
6+
7+
<p align="center" width="100%">
8+
<a href="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml">
9+
<img height="20" src="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml/badge.svg" />
10+
</a>
11+
<a href="https://github.com/constructive-io/constructive/blob/main/LICENSE">
12+
<img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/>
13+
</a>
14+
<a href="https://www.npmjs.com/package/graphile-schema">
15+
<img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=graphile%2Fgraphile-schema%2Fpackage.json"/>
16+
</a>
17+
</p>
18+
19+
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.
20+
21+
## Installation
22+
23+
```bash
24+
npm install graphile-schema
25+
```
26+
27+
## Usage
28+
29+
### Build SDL from a PostgreSQL Database
30+
31+
```typescript
32+
import { buildSchemaSDL } from 'graphile-schema';
33+
34+
const sdl = await buildSchemaSDL({
35+
database: 'mydb',
36+
schemas: ['app_public'],
37+
});
38+
39+
console.log(sdl);
40+
```
41+
42+
### Fetch SDL from a GraphQL Endpoint
43+
44+
```typescript
45+
import { fetchEndpointSchemaSDL } from 'graphile-schema';
46+
47+
const sdl = await fetchEndpointSchemaSDL('https://api.example.com/graphql', {
48+
auth: 'Bearer my-token',
49+
headers: { 'X-Custom-Header': 'value' },
50+
});
51+
52+
console.log(sdl);
53+
```
54+
55+
### With Custom Graphile Presets
56+
57+
```typescript
58+
import { buildSchemaSDL } from 'graphile-schema';
59+
60+
const sdl = await buildSchemaSDL({
61+
database: 'mydb',
62+
schemas: ['app_public', 'app_private'],
63+
graphile: {
64+
extends: [MyCustomPreset],
65+
schema: { pgSimplifyPatch: false },
66+
},
67+
});
68+
```
69+
70+
## API
71+
72+
### `buildSchemaSDL(opts)`
73+
74+
Builds a GraphQL SDL string directly from a PostgreSQL database using PostGraphile v5 introspection.
75+
76+
| Option | Type | Description |
77+
|--------|------|-------------|
78+
| `database` | `string` | Database name (default: `'constructive'`) |
79+
| `schemas` | `string[]` | PostgreSQL schemas to introspect |
80+
| `graphile` | `Partial<GraphileConfig.Preset>` | Optional Graphile preset overrides |
81+
82+
### `fetchEndpointSchemaSDL(endpoint, opts?)`
83+
84+
Fetches a GraphQL SDL string from a running GraphQL endpoint via introspection query.
85+
86+
| Option | Type | Description |
87+
|--------|------|-------------|
88+
| `endpoint` | `string` | GraphQL endpoint URL |
89+
| `opts.headerHost` | `string` | Override the `Host` header |
90+
| `opts.auth` | `string` | `Authorization` header value |
91+
| `opts.headers` | `Record<string, string>` | Additional request headers |
92+
93+
## Disclaimer
94+
95+
AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
96+
97+
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.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import * as http from 'node:http';
2+
import { getIntrospectionQuery, buildSchema, introspectionFromSchema } from 'graphql';
3+
4+
import { fetchEndpointSchemaSDL } from '../src/fetch-endpoint-schema';
5+
6+
const TEST_SDL = `
7+
type Query {
8+
hello: String
9+
version: Int
10+
}
11+
`;
12+
13+
function createMockServer(handler: (req: http.IncomingMessage, res: http.ServerResponse) => void): Promise<{ server: http.Server; port: number }> {
14+
return new Promise((resolve) => {
15+
const server = http.createServer(handler);
16+
server.listen(0, '127.0.0.1', () => {
17+
const addr = server.address() as { port: number };
18+
resolve({ server, port: addr.port });
19+
});
20+
});
21+
}
22+
23+
function introspectionHandler(_req: http.IncomingMessage, res: http.ServerResponse) {
24+
const schema = buildSchema(TEST_SDL);
25+
const introspection = introspectionFromSchema(schema);
26+
res.writeHead(200, { 'Content-Type': 'application/json' });
27+
res.end(JSON.stringify({ data: introspection }));
28+
}
29+
30+
describe('fetchEndpointSchemaSDL', () => {
31+
let server: http.Server;
32+
let port: number;
33+
34+
beforeAll(async () => {
35+
({ server, port } = await createMockServer(introspectionHandler));
36+
});
37+
38+
afterAll(() => {
39+
server.close();
40+
});
41+
42+
it('fetches and returns SDL from a live endpoint', async () => {
43+
const sdl = await fetchEndpointSchemaSDL(`http://127.0.0.1:${port}/graphql`);
44+
45+
expect(sdl).toContain('type Query');
46+
expect(sdl).toContain('hello');
47+
expect(sdl).toContain('version');
48+
});
49+
50+
it('throws on HTTP error responses', async () => {
51+
const { server: errServer, port: errPort } = await createMockServer((_req, res) => {
52+
res.writeHead(500);
53+
res.end('Internal Server Error');
54+
});
55+
56+
await expect(
57+
fetchEndpointSchemaSDL(`http://127.0.0.1:${errPort}/graphql`),
58+
).rejects.toThrow('HTTP 500');
59+
60+
errServer.close();
61+
});
62+
63+
it('throws on invalid JSON response', async () => {
64+
const { server: badServer, port: badPort } = await createMockServer((_req, res) => {
65+
res.writeHead(200, { 'Content-Type': 'text/html' });
66+
res.end('<html>not json</html>');
67+
});
68+
69+
await expect(
70+
fetchEndpointSchemaSDL(`http://127.0.0.1:${badPort}/graphql`),
71+
).rejects.toThrow('Failed to parse response');
72+
73+
badServer.close();
74+
});
75+
76+
it('throws when introspection returns errors', async () => {
77+
const { server: errServer, port: errPort } = await createMockServer((_req, res) => {
78+
res.writeHead(200, { 'Content-Type': 'application/json' });
79+
res.end(JSON.stringify({ errors: [{ message: 'Not allowed' }] }));
80+
});
81+
82+
await expect(
83+
fetchEndpointSchemaSDL(`http://127.0.0.1:${errPort}/graphql`),
84+
).rejects.toThrow('Introspection returned errors');
85+
86+
errServer.close();
87+
});
88+
89+
it('passes custom headers to the endpoint', async () => {
90+
let receivedHeaders: http.IncomingHttpHeaders = {};
91+
92+
const { server: headerServer, port: headerPort } = await createMockServer((req, res) => {
93+
receivedHeaders = req.headers;
94+
introspectionHandler(req, res);
95+
});
96+
97+
await fetchEndpointSchemaSDL(`http://127.0.0.1:${headerPort}/graphql`, {
98+
auth: 'Bearer test-token',
99+
headerHost: 'custom.host.io',
100+
headers: { 'X-Custom': 'value123' },
101+
});
102+
103+
expect(receivedHeaders['authorization']).toBe('Bearer test-token');
104+
expect(receivedHeaders['host']).toBe('custom.host.io');
105+
expect(receivedHeaders['x-custom']).toBe('value123');
106+
107+
headerServer.close();
108+
});
109+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} */
2+
module.exports = {
3+
preset: 'ts-jest',
4+
testEnvironment: 'node',
5+
transform: {
6+
'^.+\\.tsx?$': [
7+
'ts-jest',
8+
{
9+
babelConfig: false,
10+
tsconfig: 'tsconfig.json',
11+
},
12+
],
13+
},
14+
transformIgnorePatterns: [`/node_modules/*`],
15+
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
16+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
17+
modulePathIgnorePatterns: ['dist/*']
18+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "graphile-schema",
3+
"version": "1.0.0",
4+
"author": "Constructive <developers@constructive.io>",
5+
"description": "Build GraphQL SDL from PostgreSQL databases using PostGraphile v5",
6+
"main": "index.js",
7+
"module": "esm/index.js",
8+
"types": "index.d.ts",
9+
"homepage": "https://github.com/constructive-io/constructive",
10+
"license": "MIT",
11+
"publishConfig": {
12+
"access": "public",
13+
"directory": "dist"
14+
},
15+
"repository": {
16+
"type": "git",
17+
"url": "https://github.com/constructive-io/constructive"
18+
},
19+
"bugs": {
20+
"url": "https://github.com/constructive-io/constructive/issues"
21+
},
22+
"scripts": {
23+
"clean": "makage clean",
24+
"prepack": "npm run build",
25+
"build": "makage build",
26+
"build:dev": "makage build --dev",
27+
"lint": "eslint . --fix",
28+
"test": "jest",
29+
"test:watch": "jest --watch"
30+
},
31+
"dependencies": {
32+
"deepmerge": "^4.3.1",
33+
"graphile-build": "^5.0.0-rc.3",
34+
"graphile-config": "1.0.0-rc.3",
35+
"graphile-settings": "workspace:^",
36+
"graphql": "^16.9.0",
37+
"pg-cache": "workspace:^",
38+
"pg-env": "workspace:^"
39+
},
40+
"devDependencies": {
41+
"makage": "^0.1.10",
42+
"ts-node": "^10.9.2"
43+
},
44+
"keywords": [
45+
"graphile",
46+
"schema",
47+
"graphql",
48+
"sdl",
49+
"postgraphile",
50+
"introspection",
51+
"constructive"
52+
]
53+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import deepmerge from 'deepmerge'
2+
import { printSchema } from 'graphql'
3+
import { ConstructivePreset, makePgService } from 'graphile-settings'
4+
import { makeSchema } from 'graphile-build'
5+
import { buildConnectionString } from 'pg-cache'
6+
import { getPgEnvOptions } from 'pg-env'
7+
import type { GraphileConfig } from 'graphile-config'
8+
9+
export type BuildSchemaOptions = {
10+
database?: string;
11+
schemas: string[];
12+
graphile?: Partial<GraphileConfig.Preset>;
13+
};
14+
15+
export async function buildSchemaSDL(opts: BuildSchemaOptions): Promise<string> {
16+
const database = opts.database ?? 'constructive'
17+
const schemas = Array.isArray(opts.schemas) ? opts.schemas : []
18+
19+
const config = getPgEnvOptions({ database })
20+
const connectionString = buildConnectionString(
21+
config.user,
22+
config.password,
23+
config.host,
24+
config.port,
25+
config.database,
26+
)
27+
28+
const basePreset: GraphileConfig.Preset = {
29+
extends: [ConstructivePreset],
30+
pgServices: [
31+
makePgService({
32+
connectionString,
33+
schemas,
34+
}),
35+
],
36+
}
37+
38+
const preset: GraphileConfig.Preset = opts.graphile
39+
? deepmerge(basePreset, opts.graphile)
40+
: basePreset
41+
42+
const { schema } = await makeSchema(preset)
43+
return printSchema(schema)
44+
}

0 commit comments

Comments
 (0)