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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
-- requires: schemas/public/schema

BEGIN;
CREATE DOMAIN attachment AS text CHECK (VALUE ~ '^(https?)://[^\s/$.?#].[^\s]*$');
COMMENT ON DOMAIN attachment IS E'@name pgpmInternalTypeAttachment';
CREATE DOMAIN attachment AS text;
COMMENT ON DOMAIN attachment IS E'@name constructiveInternalTypeAttachment';
COMMIT;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-- requires: schemas/public/schema

BEGIN;
CREATE DOMAIN email AS citext CHECK (value ~ '^[a-zA-Z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$');
COMMENT ON DOMAIN email IS E'@name pgpmInternalTypeEmail';
CREATE DOMAIN email AS citext;
COMMENT ON DOMAIN email IS E'@name constructiveInternalTypeEmail';
COMMIT;

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-- requires: schemas/public/schema

BEGIN;
CREATE DOMAIN hostname AS text CHECK (VALUE ~ '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$');
COMMENT ON DOMAIN hostname IS E'@name pgpmInternalTypeHostname';
CREATE DOMAIN hostname AS text;
COMMENT ON DOMAIN hostname IS E'@name constructiveInternalTypeHostname';
COMMIT;

Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@
-- requires: schemas/public/schema

BEGIN;
CREATE DOMAIN image AS jsonb CHECK (
value ?& ARRAY['url', 'mime']
AND
value->>'url' ~ '^(https?)://[^\s/$.?#].[^\s]*$'
);
COMMENT ON DOMAIN image IS E'@name pgpmInternalTypeImage';
CREATE DOMAIN image AS jsonb CHECK (value ? 'url');
COMMENT ON DOMAIN image IS E'@name constructiveInternalTypeImage';
COMMIT;

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-- requires: schemas/public/schema

BEGIN;
CREATE DOMAIN origin AS text CHECK (VALUE = substring(VALUE from '^(https?://[^/]*)'));
COMMENT ON DOMAIN origin IS E'@name pgpmInternalTypeOrigin';
CREATE DOMAIN origin AS text CHECK (value ~ '^https?://[^\s]+$');
COMMENT ON DOMAIN origin IS E'@name constructiveInternalTypeOrigin';
COMMIT;

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
-- Deploy schemas/public/domains/upload to pg

-- requires: schemas/public/schema

BEGIN;

CREATE DOMAIN upload AS jsonb CHECK (
value ?& ARRAY['url', 'mime']
AND
value->>'url' ~ '^(https?)://[^\s/$.?#].[^\s]*$'
);
COMMENT ON DOMAIN upload IS E'@name pgpmInternalTypeUpload';

CREATE DOMAIN upload AS jsonb CHECK (value ? 'url' OR value ? 'id' OR value ? 'key');
COMMENT ON DOMAIN upload IS E'@name constructiveInternalTypeUpload';
COMMIT;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-- requires: schemas/public/schema

BEGIN;
CREATE DOMAIN url AS text CHECK (VALUE ~ '^(https?)://[^\s/$.?#].[^\s]*$');
COMMENT ON DOMAIN url IS E'@name pgpmInternalTypeUrl';
CREATE DOMAIN url AS text CHECK (value ~ '^https?://[^\s]+$');
COMMENT ON DOMAIN url IS E'@name constructiveInternalTypeUrl';
COMMIT;

Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ schemas/public/domains/attachment [schemas/public/schema] 2017-08-11T08:11:51Z s
schemas/public/domains/email [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/email
schemas/public/domains/hostname [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/hostname
schemas/public/domains/image [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/image
schemas/public/domains/multiple_select [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/multiple_select
schemas/public/domains/origin [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/origin
schemas/public/domains/single_select [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/single_select
schemas/public/domains/upload [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/upload
schemas/public/domains/url [schemas/public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/public/domains/url

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

7 changes: 7 additions & 0 deletions graphile/graphile-settings/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,10 @@ export {
TsvectorCodecPlugin,
TsvectorCodecPreset,
} from './tsvector-codec';

// PG type mappings for custom PostgreSQL types (email, url, etc.)
export {
PgTypeMappingsPlugin,
PgTypeMappingsPreset,
} from './pg-type-mappings';
export type { TypeMapping } from './pg-type-mappings';
154 changes: 154 additions & 0 deletions graphile/graphile-settings/src/plugins/pg-type-mappings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import type { GraphileConfig } from 'graphile-config';
import { GraphQLString } from 'grafast/graphql';
import sql from 'pg-sql2';

/**
* Type mapping configuration for custom PostgreSQL types.
*/
export interface TypeMapping {
/** PostgreSQL type name */
name: string;
/** PostgreSQL schema/namespace name */
namespaceName: string;
/** GraphQL type to map to */
type: 'String';
}

/**
* Default type mappings for common custom PostgreSQL types.
* These are typically domain types or composite types that should be
* represented as simple scalars in GraphQL.
*/
const DEFAULT_MAPPINGS: TypeMapping[] = [
{ name: 'email', namespaceName: 'public', type: 'String' },
{ name: 'hostname', namespaceName: 'public', type: 'String' },
{ name: 'origin', namespaceName: 'public', type: 'String' },
{ name: 'url', namespaceName: 'public', type: 'String' },
];

/**
* Plugin that maps custom PostgreSQL types to GraphQL scalar types.
*
* This is useful for domain types or composite types that should be
* represented as simple scalars (String, JSON) in the GraphQL API.
*
* For example, if you have:
* CREATE DOMAIN email AS text;
* CREATE TYPE url AS (value text);
*
* This plugin will map them to GraphQL String type instead of creating
* complex object types.
*
* The plugin handles both:
* 1. Domain types (simple aliases) - maps directly to the target scalar
* 2. Composite types - extracts the first field's value when converting from PG
*/
export const PgTypeMappingsPlugin: GraphileConfig.Plugin = {
name: 'PgTypeMappingsPlugin',
version: '1.0.0',

gather: {
hooks: {
async pgCodecs_findPgCodec(info, event) {
if (event.pgCodec) {
return;
}

const { pgType: type, serviceName } = event;

// Find the namespace for this type
const namespace = await info.helpers.pgIntrospection.getNamespace(
serviceName,
type.typnamespace
);

if (!namespace) {
return;
}

// Check if this type matches any of our mappings
const mapping = DEFAULT_MAPPINGS.find(
m => m.name === type.typname && m.namespaceName === namespace.nspname
);

if (!mapping) {
return;
}

// Create a codec for this type
// For composite types, the fromPg function extracts the first field's value
// For domain types, it just passes through the value
event.pgCodec = {
name: type.typname,
sqlType: sql.identifier(namespace.nspname, type.typname),
fromPg: (value: unknown) => {
if (value == null) {
return null;
}
// If it's already a scalar, return it
if (typeof value !== 'object' || Array.isArray(value)) {
return value;
}
// For composite types, extract the first field's value
const obj = value as Record<string, unknown>;
const keys = Object.keys(obj);
if (keys.length > 0) {
return obj[keys[0]];
}
return value;
},
toPg: (value: unknown) => value as string,
attributes: undefined,
executor: null,
extensions: {
oid: type._id,
pg: {
serviceName,
schemaName: namespace.nspname,
name: type.typname,
},
tags: {
// Mark this as a custom mapped type
pgTypeMappings: mapping.type,
},
},
};
},
},
},

schema: {
hooks: {
init(_, build) {
const { setGraphQLTypeForPgCodec } = build;

// Map our custom codecs to GraphQL types
for (const codec of Object.values(build.input.pgRegistry.pgCodecs)) {
const mappingType = codec.extensions?.tags?.pgTypeMappings as string | undefined;
if (mappingType) {
const gqlTypeName = GraphQLString.name;
setGraphQLTypeForPgCodec(codec, 'input', gqlTypeName);
setGraphQLTypeForPgCodec(codec, 'output', gqlTypeName);
}
}

return _;
},
},
},
};

/**
* Preset that includes the PG type mappings plugin.
*
* This preset maps common custom PostgreSQL types to GraphQL scalars:
* - email -> String
* - hostname -> String
* - url -> String
* - origin -> String
*/
export const PgTypeMappingsPreset: GraphileConfig.Preset = {
plugins: [PgTypeMappingsPlugin],
};

export default PgTypeMappingsPlugin;
3 changes: 3 additions & 0 deletions graphile/graphile-settings/src/presets/constructive-preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { EnableAllFilterColumnsPreset } from '../plugins/enable-all-filter-colum
import { ManyToManyOptInPreset } from '../plugins/many-to-many-preset';
import { MetaSchemaPreset } from '../plugins/meta-schema';
import { TsvectorCodecPreset } from '../plugins/tsvector-codec';
import { PgTypeMappingsPreset } from '../plugins/pg-type-mappings';

/**
* Constructive PostGraphile v5 Preset
Expand All @@ -25,6 +26,7 @@ import { TsvectorCodecPreset } from '../plugins/tsvector-codec';
* - Connection filter plugin with all columns filterable
* - Many-to-many relationships (opt-in via @behavior +manyToMany)
* - Meta schema plugin (_meta query for introspection of tables, fields, indexes)
* - PG type mappings (maps custom types like email, url to GraphQL scalars)
*
* DISABLED PLUGINS:
* - PgConnectionArgFilterBackwardRelationsPlugin (relation filters bloat the API)
Expand Down Expand Up @@ -58,6 +60,7 @@ export const ConstructivePreset: GraphileConfig.Preset = {
ManyToManyOptInPreset,
MetaSchemaPreset,
TsvectorCodecPreset,
PgTypeMappingsPreset,
],
/**
* Disable relation filter plugins from postgraphile-plugin-connection-filter.
Expand Down