Browser-safe GraphQL query generation core for PostGraphile schemas. Build type-safe queries, mutations, and introspection pipelines — at runtime or build time.
npm install @constructive-io/graphql-queryThis package is the canonical source for PostGraphile query generation logic. It provides:
- Query generators —
buildSelect,buildFindOne,buildCountfor read operations - Mutation generators —
buildPostGraphileCreate,buildPostGraphileUpdate,buildPostGraphileDelete - Introspection pipeline —
inferTablesFromIntrospectionto convert a GraphQL schema intoCleanTablemetadata - AST builders — low-level
getAll,getMany,getOne,createOne,patchOne,deleteOne - Client utilities —
TypedDocumentString,execute, and@constructive-io/errorsre-exports (ConstructiveError,parseGraphQLError) for type-safe execution and error handling - Naming helpers — server-aware inflection functions that respect PostGraphile's schema naming
All modules are browser-safe (no Node.js APIs). @constructive-io/graphql-codegen depends on this package for the core logic and adds Node.js-only features (CLI, file output, watch mode).
The most common workflow: introspect a GraphQL schema, then generate queries and mutations for any table.
import {
inferTablesFromIntrospection,
SCHEMA_INTROSPECTION_QUERY,
} from '@constructive-io/graphql-query';
// Fetch introspection from any GraphQL endpoint
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: SCHEMA_INTROSPECTION_QUERY }),
});
const { data } = await response.json();
// Convert to CleanTable metadata
const tables = inferTablesFromIntrospection(data);
// tables = [{ name: 'User', fields: [...], relations: {...}, query: {...}, inflection: {...} }, ...]import { buildSelect } from '@constructive-io/graphql-query';
const userTable = tables.find(t => t.name === 'User')!;
const query = buildSelect(userTable, tables);
console.log(query.toString());Generated GraphQL:
query getUsersQuery(
$first: Int
$last: Int
$after: Cursor
$before: Cursor
$offset: Int
$condition: UserCondition
$filter: UserFilter
$orderBy: [UsersOrderBy!]
) {
users(
first: $first
last: $last
offset: $offset
after: $after
before: $before
condition: $condition
filter: $filter
orderBy: $orderBy
) {
totalCount
pageInfo {
hasNextPage
hasPreviousPage
endCursor
startCursor
}
nodes {
id
name
email
createdAt
}
}
}import {
buildPostGraphileCreate,
buildPostGraphileUpdate,
buildPostGraphileDelete,
} from '@constructive-io/graphql-query';
const createQuery = buildPostGraphileCreate(userTable, tables);
const updateQuery = buildPostGraphileUpdate(userTable, tables);
const deleteQuery = buildPostGraphileDelete(userTable, tables);Generated CREATE mutation:
mutation createUserMutation($input: CreateUserInput!) {
createUser(input: $input) {
user {
id
name
email
createdAt
}
}
}Generated UPDATE mutation:
mutation updateUserMutation($input: UpdateUserInput!) {
updateUser(input: $input) {
user {
id
name
email
createdAt
}
}
}Generated DELETE mutation:
mutation deleteUserMutation($input: DeleteUserInput!) {
deleteUser(input: $input) {
clientMutationId
}
}Include related tables in your query with automatic Connection wrapping for hasMany relations:
import { buildSelect } from '@constructive-io/graphql-query';
const actionTable = tables.find(t => t.name === 'Action')!;
const query = buildSelect(actionTable, tables, {
fieldSelection: {
select: ['id', 'name', 'photo', 'title'],
include: {
actionResults: ['id', 'actionId'], // hasMany → wrapped in nodes { ... }
category: true, // belongsTo → direct nesting
},
},
});Generated GraphQL:
query actionsQuery {
actions {
totalCount
nodes {
id
name
photo
title
actionResults(first: 20) {
nodes {
id
actionId
}
}
category {
id
name
}
}
}
}hasMany relations are automatically wrapped in the PostGraphile Connection pattern (
nodes { ... }with a defaultfirst: 20limit). belongsTo relations are nested directly.
import { buildFindOne } from '@constructive-io/graphql-query';
const findOneQuery = buildFindOne(userTable);
console.log(findOneQuery.toString());Generated GraphQL:
query getUserQuery($id: UUID!) {
user(id: $id) {
id
name
email
createdAt
}
}import { buildCount } from '@constructive-io/graphql-query';
const countQuery = buildCount(userTable);
console.log(countQuery.toString());Generated GraphQL:
query getUsersCountQuery(
$condition: UserCondition
$filter: UserFilter
) {
users(condition: $condition, filter: $filter) {
totalCount
}
}Control which fields and relations are included using presets or custom selection:
import { buildSelect } from '@constructive-io/graphql-query';
// Preset: just id + a few display fields
const minimal = buildSelect(userTable, tables, { fieldSelection: 'minimal' });
// Preset: all scalar fields (no relations)
const allFields = buildSelect(userTable, tables, { fieldSelection: 'all' });
// Preset: everything including relations
const full = buildSelect(userTable, tables, { fieldSelection: 'full' });
// Custom: pick specific fields + exclude some + include specific relations
const custom = buildSelect(userTable, tables, {
fieldSelection: {
select: ['id', 'name', 'email'],
exclude: ['internalNotes'],
include: { posts: ['id', 'title'] },
},
});Remap relation field names when the server-side name differs from what your consumer expects:
const query = buildSelect(userTable, tables, {
relationFieldMap: {
contact: 'contactByOwnerId', // emits: contact: contactByOwnerId { ... }
internalNotes: null, // omits this relation entirely
},
});import { createGraphQLClient } from '@constructive-io/graphql-query';
const client = createGraphQLClient({
url: '/graphql',
headers: { Authorization: `Bearer ${token}` },
});
const { data, errors } = await client.execute(query, {
first: 10,
filter: { name: { includesInsensitive: 'search' } },
});Error handling is backed by @constructive-io/errors, the
single source of truth for error codes, their public/internal class, message
copy (i18n), and cross-source parsing. There is no separate client-side error
catalog or message string-matching — the client re-exports the canonical
ConstructiveError, parse, format, and helpers.
import {
ConstructiveError,
parseGraphQLError,
format,
isRetryable,
} from '@constructive-io/graphql-query';
try {
const result = await client.execute(createQuery, { input: { user: { name: 'Alice' } } });
if (result.errors) {
// Normalize any error into a ConstructiveError (machine code + context + class).
const error = parseGraphQLError(result.errors[0]);
if (error.code === 'UNIQUE_VIOLATION') {
console.log('Duplicate entry:', error.context.constraint);
}
}
} catch (err) {
if (err instanceof ConstructiveError) {
// Localized, user-facing copy for public codes; retry hint for transport errors.
console.log(err.code, format(err.code, err.context));
if (isRetryable(err)) {
// network/timeout/rate-limit — safe to retry
}
}
}All generators automatically use server-inferred names from introspection when available, falling back to local inflection conventions:
import {
toCamelCaseSingular,
toCamelCasePlural,
toCreateMutationName,
toPatchFieldName,
toFilterTypeName,
} from '@constructive-io/graphql-query';
// Uses table.query/table.inflection if available, falls back to convention
toCamelCaseSingular('DeliveryZone', table); // "deliveryZone" (from table.inflection.tableFieldName)
toCamelCasePlural('DeliveryZone', table); // "deliveryZones" (from table.query.all)
toCreateMutationName('User', table); // "createUser" (from table.query.create)
toPatchFieldName('User', table); // "userPatch" (from table.query.patchFieldName)
toFilterTypeName('User', table); // "UserFilter" (from table.inflection.filterType)GraphQL Schema (introspection or _meta)
|
v
inferTablesFromIntrospection()
|
v
CleanTable[] (normalized metadata with inflection + query names)
|
v
buildSelect / buildFindOne / buildCount / buildPostGraphileCreate / ...
|
v
gql-ast (AST node factories)
|
v
graphql print() -> query string
|
v
TypedDocumentString (type-safe wrapper)
@constructive-io/graphql-query (this package — browser-safe core)
|
v
@constructive-io/graphql-codegen (Node.js CLI — depends on graphql-query)
+ CLI entry points
+ File output (writes .ts files to disk)
+ Watch mode
+ Database introspection
+ React Query hook generation
+ Babel codegen templates
| Function | Description |
|---|---|
buildSelect(table, allTables, options?) |
Build a paginated SELECT query with filters, sorting, field selection |
buildFindOne(table, pkField?) |
Build a single-row query by primary key |
buildCount(table) |
Build a count query with optional condition/filter |
buildPostGraphileCreate(table, allTables, options?) |
Build a CREATE mutation |
buildPostGraphileUpdate(table, allTables, options?) |
Build an UPDATE mutation |
buildPostGraphileDelete(table, allTables, options?) |
Build a DELETE mutation |
| Function | Description |
|---|---|
inferTablesFromIntrospection(data, options?) |
Convert GraphQL introspection to CleanTable[] |
transformSchemaToOperations(types, queryType, mutationType) |
Extract operations from schema types |
SCHEMA_INTROSPECTION_QUERY |
Full introspection query string |
| Export | Description |
|---|---|
TypedDocumentString |
Type-safe GraphQL document wrapper (compatible with codegen client) |
createGraphQLClient(options) |
Create a typed GraphQL client |
execute(url, query, variables, options?) |
Execute a GraphQL query |
ConstructiveError |
Canonical error class (code, context, class, http) from @constructive-io/errors |
parseGraphQLError(error) |
Parse any error (pg / GraphQL / message / SQLSTATE) into a ConstructiveError |
| Function | Description |
|---|---|
toCamelCaseSingular(name, table?) |
Server-aware singular name |
toCamelCasePlural(name, table?) |
Server-aware plural name |
toCreateMutationName(name, table?) |
Create mutation operation name |
toUpdateMutationName(name, table?) |
Update mutation operation name |
toDeleteMutationName(name, table?) |
Delete mutation operation name |
toPatchFieldName(name, table?) |
Patch field name in update input |
toFilterTypeName(name, table?) |
Filter type name |
toOrderByEnumValue(fieldName) |
Convert field name to OrderBy enum value |
normalizeInflectionValue(value, fallback) |
Safe inflection value lookup |
| Type | Description |
|---|---|
CleanTable |
Normalized table metadata with fields, relations, inflection, query names |
CleanField |
Field with type info, isNotNull, hasDefault |
QueryOptions |
Options for buildSelect (pagination, filters, field selection, relation mapping) |
MutationOptions |
Options for mutation builders |
FieldSelection |
Field selection presets and custom configuration |
ConnectionResult<T> |
Relay-style connection result type |
Filter |
PostGraphile connection filter type |