Skip to content
Open
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

Large diffs are not rendered by default.

379 changes: 374 additions & 5 deletions graphile/graphile-misc-plugins/__tests__/meta-schema.test.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions graphile/graphile-misc-plugins/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ export type { UniqueLookupOptions } from './primary-key-only';
export {
MetaSchemaPlugin,
MetaSchemaPreset,
getCachedTablesMeta,
} from './meta-schema';
export type { TableMeta } from './meta-schema';

// PG type mappings for custom PostgreSQL types (email, url, etc.)
export {
Expand Down
4 changes: 3 additions & 1 deletion graphile/graphile-misc-plugins/src/meta-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
*/

import type { GraphileConfig } from 'graphile-config';
import { cachedTablesMeta } from './meta-schema/cache';
import { cachedTablesMeta, getCachedTablesMeta } from './meta-schema/cache';
import { MetaSchemaPlugin } from './meta-schema/plugin';
import { buildFieldMeta, pgTypeToGqlType } from './meta-schema/type-mappings';

export { MetaSchemaPlugin };
export { getCachedTablesMeta };
export type { TableMeta } from './meta-schema/types';

export const MetaSchemaPreset: GraphileConfig.Preset = {
plugins: [MetaSchemaPlugin],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { resolveTableType } from './name-meta-builders';
import { buildFieldMeta } from './type-mappings';
import { buildFieldList, type BuildContext } from './table-meta-context';
import type {
Expand All @@ -20,7 +21,9 @@ export function buildForeignKeyConstraint(
remoteAttributeNames: string[],
context: BuildContext,
): ForeignKeyConstraintMeta {
const referencedTable = remoteCodec?.name || 'unknown';
const referencedTable = remoteCodec
? resolveTableType(context.build, remoteCodec)
: 'unknown';
const referencedFields = remoteAttributeNames.map((attrName) =>
remoteCodec ? context.inflectAttr(attrName, remoteCodec) : attrName,
);
Expand Down Expand Up @@ -94,7 +97,7 @@ export function buildForeignKeyConstraints(
const constraints: ForeignKeyConstraintMeta[] = [];

for (const [relationName, relation] of Object.entries(relations)) {
if (relation.isReferencee !== false) continue;
if (relation.isReferencee === true) continue;

const remoteCodec = relation.remoteResource?.codec;
const remoteAttributes = remoteCodec?.attributes || {};
Expand Down
1 change: 1 addition & 0 deletions graphile/graphile-misc-plugins/src/meta-schema/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const MetaSchemaPlugin: GraphileConfig.Plugin = {
name: 'MetaSchemaPlugin',
version: '1.0.0',
description: 'Exposes _meta query for database schema introspection',
after: ['PgManyToManyRelationPlugin'],
schema: {
hooks: {
init(input, rawBuild) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { safeInflection } from './inflection-utils';
import {
buildForeignKeyConstraint,
} from './constraint-meta-builders';
import { resolveTableType } from './name-meta-builders';
import { buildFieldList, type BuildContext } from './table-meta-context';
import {
getRelation,
Expand All @@ -22,6 +23,15 @@ import type {
PgUnique,
} from './types';

function resolveRemoteTableName(
remoteResource: PgTableResource | null | undefined,
context: BuildContext,
): string {
const codec = remoteResource?.codec;
if (!codec) return 'unknown';
return resolveTableType(context.build, codec);
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
Expand All @@ -36,19 +46,21 @@ export function buildBelongsToRelations(
const belongsTo: BelongsToRelation[] = [];

for (const [relationName, relation] of Object.entries(relations)) {
if (relation.isReferencee !== false) continue;
if (relation.isReferencee === true) continue;

const localAttributes = relation.localAttributes || [];
const isUnique = uniques.some((unique) =>
sameAttributes(unique.attributes, localAttributes),
);

const remoteTableName = resolveRemoteTableName(relation.remoteResource, context);

belongsTo.push({
fieldName: relationName,
isUnique,
type: relation.remoteResource?.codec?.name || null,
type: remoteTableName,
keys: buildFieldList(localAttributes, codec, attributes, context),
references: { name: relation.remoteResource?.codec?.name || 'unknown' },
references: { name: remoteTableName },
});
}

Expand All @@ -73,12 +85,14 @@ export function buildReverseRelations(
sameAttributes(unique.attributes, remoteAttributes),
);

const remoteTableName = resolveRemoteTableName(relation.remoteResource, context);

const meta: HasRelation = {
fieldName: relationName,
isUnique,
type: relation.remoteResource?.codec?.name || null,
type: remoteTableName,
keys: buildFieldList(relation.localAttributes || [], codec, attributes, context),
referencedBy: { name: relation.remoteResource?.codec?.name || 'unknown' },
referencedBy: { name: remoteTableName },
};

if (isUnique) {
Expand Down Expand Up @@ -174,10 +188,13 @@ function buildManyToManyRelation(
context,
);

const rightTableType = resolveTableType(context.build, rightCodec);
const junctionTableType = resolveTableType(context.build, junctionCodec);

return {
fieldName: relationFieldName,
type: rightCodec.name || null,
junctionTable: { name: junctionCodec.name || 'unknown' },
type: rightTableType,
junctionTable: { name: junctionTableType },
junctionLeftConstraint,
junctionLeftKeyAttributes: buildFieldList(
leftJunctionAttributes,
Expand All @@ -204,7 +221,7 @@ function buildManyToManyRelation(
rightCodec.attributes,
context,
),
rightTable: { name: rightCodec.name || 'unknown' },
rightTable: { name: rightTableType },
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function buildTableMeta(
const codec = resource.codec;
const attributes = codec.attributes;
const uniques = getUniques(resource);
const relations = getRelations(resource);
const relations = getRelations(resource, context.build.input.pgRegistry.pgRelations);

const fields = Object.entries(attributes).map(([attrName, attr]) =>
buildFieldMeta(context.inflectAttr(attrName, codec), attr, context.build),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,21 @@ export function getUniques(resource: PgTableResource): PgUnique[] {
return Array.isArray(resource.uniques) ? resource.uniques : [];
}

export function getRelations(resource: PgTableResource): Record<string, PgRelation> {
return resource.relations || resource.getRelations?.() || {};
export function getRelations(
resource: PgTableResource,
pgRelations?: Record<string, Record<string, PgRelation>>,
): Record<string, PgRelation> {
const fromMethod = resource.getRelations?.();
if (fromMethod && Object.keys(fromMethod).length > 0) return fromMethod;

// Direct registry lookup by codec name
const codecName = resource.codec?.name;
if (codecName && pgRelations?.[codecName]) {
const fromRegistry = pgRelations[codecName];
if (Object.keys(fromRegistry).length > 0) return fromRegistry;
}

return resource.relations || {};
}

export function getRelation(resource: PgTableResource, relationName: string): PgRelation | null {
Expand Down
1 change: 1 addition & 0 deletions graphile/graphile-misc-plugins/src/meta-schema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export interface MetaBuild extends GqlTypeResolverBuild {
input: {
pgRegistry: {
pgResources: Record<string, PgTableResource>;
pgRelations?: Record<string, Record<string, PgRelation>>;
};
};
inflection: MetaInflection;
Expand Down
1 change: 1 addition & 0 deletions graphile/graphile-schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"deepmerge": "^4.3.1",
"graphile-build": "5.0.0-rc.6",
"graphile-config": "1.0.0-rc.6",
"graphile-misc-plugins": "workspace:^",
"graphile-settings": "workspace:^",
"graphql": "16.13.0",
"pg-cache": "workspace:^",
Expand Down
32 changes: 30 additions & 2 deletions graphile/graphile-schema/src/build-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,28 @@ import { makeSchema } from 'graphile-build'
import { buildConnectionString } from 'pg-cache'
import { getPgEnvOptions } from 'pg-env'
import type { GraphileConfig } from 'graphile-config'
import { getCachedTablesMeta } from 'graphile-misc-plugins';
import type { TableMeta } from 'graphile-misc-plugins';

export type BuildSchemaOptions = {
database?: string;
schemas: string[];
graphile?: Partial<GraphileConfig.Preset>;
};

export async function buildSchemaSDL(opts: BuildSchemaOptions): Promise<string> {
export interface BuildSchemaResult {
sdl: string;
tablesMeta: TableMeta[];
}

/**
* Build a GraphQL schema from a PostgreSQL database and return both
* the SDL string and the table metadata collected by MetaSchemaPlugin.
*
* The tablesMeta is captured immediately after makeSchema() returns,
* before the module-level cache can be overwritten by concurrent calls.
*/
export async function buildSchemaWithMeta(opts: BuildSchemaOptions): Promise<BuildSchemaResult> {
const database = opts.database ?? 'constructive'
const schemas = Array.isArray(opts.schemas) ? opts.schemas : []

Expand Down Expand Up @@ -40,5 +54,19 @@ export async function buildSchemaSDL(opts: BuildSchemaOptions): Promise<string>
: basePreset

const { schema } = await makeSchema(preset)
return printSchema(schema)
// Capture tablesMeta immediately — the MetaSchemaPlugin's init hook
// populates cachedTablesMeta during makeSchema(). Grab it now before
// any concurrent call to makeSchema() overwrites the module-level cache.
const tablesMeta = getCachedTablesMeta();
const sdl = printSchema(schema)
return { sdl, tablesMeta };
}

/**
* Build a GraphQL schema SDL string from a PostgreSQL database.
* For backward compatibility — use buildSchemaWithMeta() when you also need table metadata.
*/
export async function buildSchemaSDL(opts: BuildSchemaOptions): Promise<string> {
const { sdl } = await buildSchemaWithMeta(opts);
return sdl;
}
5 changes: 3 additions & 2 deletions graphile/graphile-schema/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { buildSchemaSDL } from './build-schema';
export type { BuildSchemaOptions } from './build-schema';
export { buildSchemaSDL, buildSchemaWithMeta } from './build-schema';
export type { BuildSchemaOptions, BuildSchemaResult } from './build-schema';
export type { TableMeta } from 'graphile-misc-plugins';
export { fetchEndpointSchemaSDL } from './fetch-endpoint-schema';
export type { FetchEndpointSchemaOptions } from './fetch-endpoint-schema';
2 changes: 1 addition & 1 deletion graphql/codegen/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = {
},
transformIgnorePatterns: [`/node_modules/*`],
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
testPathIgnorePatterns: ['/node_modules/', '/__tests__/fixtures/'],
testPathIgnorePatterns: ['/node_modules/', '/fixtures/'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
modulePathIgnorePatterns: ['dist/*']
};
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,31 @@ export type CommentSelect = {
select: UserSelect;
};
};
// ============ ToMany Relational Filter Types ============
export interface UserToManyPostFilter {
/** Every related item must match this filter */
every?: PostFilter;
/** At least one related item must match this filter */
some?: PostFilter;
/** No related items may match this filter */
none?: PostFilter;
}
export interface UserToManyCommentFilter {
/** Every related item must match this filter */
every?: CommentFilter;
/** At least one related item must match this filter */
some?: CommentFilter;
/** No related items may match this filter */
none?: CommentFilter;
}
export interface PostToManyCommentFilter {
/** Every related item must match this filter */
every?: CommentFilter;
/** At least one related item must match this filter */
some?: CommentFilter;
/** No related items may match this filter */
none?: CommentFilter;
}
// ============ Table Filter Types ============
export interface UserFilter {
id?: UUIDFilter;
Expand All @@ -348,6 +373,8 @@ export interface UserFilter {
isActive?: BooleanFilter;
createdAt?: DatetimeFilter;
metadata?: JSONFilter;
posts?: UserToManyPostFilter;
comments?: UserToManyCommentFilter;
and?: UserFilter[];
or?: UserFilter[];
not?: UserFilter;
Expand All @@ -359,6 +386,8 @@ export interface PostFilter {
authorId?: UUIDFilter;
publishedAt?: DatetimeFilter;
tags?: StringFilter;
author?: UserFilter;
comments?: PostToManyCommentFilter;
and?: PostFilter[];
or?: PostFilter[];
not?: PostFilter;
Expand All @@ -369,6 +398,8 @@ export interface CommentFilter {
postId?: UUIDFilter;
authorId?: UUIDFilter;
createdAt?: DatetimeFilter;
post?: PostFilter;
author?: UserFilter;
and?: CommentFilter[];
or?: CommentFilter[];
not?: CommentFilter;
Expand Down Expand Up @@ -1793,6 +1824,15 @@ export type ProfileSelect = {
select: UserSelect;
};
};
// ============ ToMany Relational Filter Types ============
export interface UserToManyPostFilter {
/** Every related item must match this filter */
every?: PostFilter;
/** At least one related item must match this filter */
some?: PostFilter;
/** No related items may match this filter */
none?: PostFilter;
}
// ============ Table Filter Types ============
export interface UserFilter {
id?: UUIDFilter;
Expand All @@ -1802,6 +1842,8 @@ export interface UserFilter {
isActive?: BooleanFilter;
createdAt?: DatetimeFilter;
metadata?: JSONFilter;
profile?: ProfileFilter;
posts?: UserToManyPostFilter;
and?: UserFilter[];
or?: UserFilter[];
not?: UserFilter;
Expand All @@ -1811,6 +1853,7 @@ export interface ProfileFilter {
bio?: StringFilter;
userId?: UUIDFilter;
avatarUrl?: StringFilter;
user?: UserFilter;
and?: ProfileFilter[];
or?: ProfileFilter[];
not?: ProfileFilter;
Expand Down Expand Up @@ -2190,6 +2233,15 @@ export type CategorySelect = {
orderBy?: PostsOrderBy[];
};
};
// ============ ToMany Relational Filter Types ============
export interface PostToManyCommentFilter {
/** Every related item must match this filter */
every?: CommentFilter;
/** At least one related item must match this filter */
some?: CommentFilter;
/** No related items may match this filter */
none?: CommentFilter;
}
// ============ Table Filter Types ============
export interface PostFilter {
id?: UUIDFilter;
Expand All @@ -2198,6 +2250,8 @@ export interface PostFilter {
authorId?: UUIDFilter;
publishedAt?: DatetimeFilter;
tags?: StringFilter;
author?: UserFilter;
comments?: PostToManyCommentFilter;
and?: PostFilter[];
or?: PostFilter[];
not?: PostFilter;
Expand Down
Loading