Skip to content

Commit 35905be

Browse files
tsnobipben-pr-pbenjie
authored
feat(pg): use table enums in functions via domain (#832)
Co-authored-by: ben-pr-p <ben.paul.ryan.packer@gmail.com> Co-authored-by: Benjie <code@benjiegillam.com>
1 parent 7427231 commit 35905be

7 files changed

Lines changed: 650 additions & 26 deletions

File tree

packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,20 @@ export type PgEntity =
209209
| PgExtension
210210
| PgIndex;
211211

212+
const fakeEnumIdentifier = (
213+
namespaceName: string,
214+
name: string,
215+
identifierExtra = "",
216+
list = false
217+
) => {
218+
const baseEnumIdentifier = `FAKE_ENUM_${namespaceName}_${name}${identifierExtra}`;
219+
if (list) {
220+
return `${baseEnumIdentifier}_list`;
221+
} else {
222+
return baseEnumIdentifier;
223+
}
224+
};
225+
212226
export type PgIntrospectionResultsByKind = {
213227
__pgVersion: number,
214228
attribute: PgAttribute[],
@@ -556,7 +570,12 @@ function enumTables(introspectionResults) {
556570
const enumTypeArray = {
557571
kind: "type",
558572
isFake: true,
559-
id: `FAKE_ENUM_${klass.namespaceName}_${klass.name}${constraintIdent}_list`,
573+
id: fakeEnumIdentifier(
574+
klass.namespaceName,
575+
klass.name,
576+
constraintIdent,
577+
true
578+
),
560579
name: `_${klass.name}${constraintIdent}`,
561580
description: null,
562581
tags: {},
@@ -579,7 +598,12 @@ function enumTables(introspectionResults) {
579598
const enumType = {
580599
kind: "type",
581600
isFake: true,
582-
id: `FAKE_ENUM_${klass.namespaceName}_${klass.name}${constraintIdent}`,
601+
id: fakeEnumIdentifier(
602+
klass.namespaceName,
603+
klass.name,
604+
constraintIdent,
605+
false
606+
),
583607
name: `${klass.name}${constraintIdent}`,
584608
description: klass.description,
585609
tags: { ...klass.tags, ...constraint.tags },
@@ -1320,6 +1344,7 @@ Original error: ${e.message}
13201344
rawIntrospectionResultsByKind,
13211345
build.pgAugmentIntrospectionResults
13221346
);
1347+
13231348
if (introspectionResultsByKind.__pgVersion < 90500) {
13241349
// TODO:v5: remove this workaround
13251350
// This is a bit of a hack, but until we have plugin priorities it's the
@@ -1330,6 +1355,7 @@ Original error: ${e.message}
13301355
}
13311356
return build.extend(build, {
13321357
pgIntrospectionResultsByKind: introspectionResultsByKind,
1358+
getPgFakeEnumIdentifier: fakeEnumIdentifier,
13331359
});
13341360
},
13351361
["PgIntrospection"],

packages/graphile-build-pg/src/plugins/PgTypesPlugin.js

Lines changed: 76 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import makeGraphQLJSONType from "../GraphQLJSON";
55

66
import { parseInterval } from "../postgresInterval";
77

8+
const ENUM_DOMAIN_SUFFIX = "_enum_domain";
9+
810
function indent(str) {
911
return " " + str.replace(/\n/g, "\n ");
1012
}
@@ -30,6 +32,7 @@ export default (function PgTypesPlugin(
3032
build => {
3133
const {
3234
pgIntrospectionResultsByKind: introspectionResultsByKind,
35+
getPgFakeEnumIdentifier,
3336
getTypeByName,
3437
pgSql: sql,
3538
inflection,
@@ -753,6 +756,10 @@ export default (function PgTypesPlugin(
753756
gqlInputType;
754757
}
755758
}
759+
760+
// cf https://github.com/graphile/graphile-engine/pull/748#discussion_r650828353
761+
let shouldSkipAddType = false;
762+
756763
// Enums
757764
if (
758765
!gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] &&
@@ -994,30 +1001,72 @@ end`;
9941001
type.type === "d" &&
9951002
type.domainBaseTypeId
9961003
) {
997-
const baseType = getGqlTypeByTypeIdAndModifier(
998-
type.domainBaseTypeId,
999-
typeModifier
1000-
);
1001-
const baseInputType = getGqlInputTypeByTypeIdAndModifier(
1002-
type.domainBaseTypeId,
1003-
typeModifier
1004-
);
1005-
// Hack stolen from: https://github.com/graphile/postgraphile/blob/ade728ed8f8e3ecdc5fdad7d770c67aa573578eb/src/graphql/schema/type/aliasGqlType.ts#L16
1006-
gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = Object.assign(
1007-
Object.create(baseType),
1008-
{
1009-
name: inflection.domainType(type),
1010-
description: type.description,
1004+
// might be used as an enum alias: https://github.com/graphile/postgraphile/issues/1500
1005+
const tagEnumName =
1006+
type.tags && typeof type.tags.enum === "string"
1007+
? type.tags.enum
1008+
: null;
1009+
const truncatedTypeName =
1010+
!tagEnumName && type.name.endsWith(ENUM_DOMAIN_SUFFIX)
1011+
? type.name.slice(0, type.name.length - ENUM_DOMAIN_SUFFIX.length)
1012+
: null;
1013+
const underlyingEnumTypeName = tagEnumName || truncatedTypeName;
1014+
if (underlyingEnumTypeName !== null) {
1015+
const baseTypeId = getPgFakeEnumIdentifier(
1016+
type.namespaceName,
1017+
underlyingEnumTypeName
1018+
);
1019+
1020+
const baseType = getGqlTypeByTypeIdAndModifier(
1021+
baseTypeId,
1022+
typeModifierKey
1023+
);
1024+
1025+
const baseInputType = getGqlInputTypeByTypeIdAndModifier(
1026+
baseTypeId,
1027+
typeModifierKey
1028+
);
1029+
1030+
if (!baseType || !baseInputType) {
1031+
if (tagEnumName) {
1032+
throw new Error(
1033+
`The domain ${type.name} uses '@enum ${tagEnumName}' to references its underlying enum type, but no type named ${tagEnumName} could be found.`
1034+
);
1035+
} else {
1036+
throw new Error(
1037+
`The domain ${type.name} references the enum type ${underlyingEnumTypeName}, but no such type could be found.`
1038+
);
1039+
}
10111040
}
1012-
);
1013-
if (baseInputType && baseInputType !== baseType) {
1041+
1042+
gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = baseType;
10141043
gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey] =
1015-
Object.assign(Object.create(baseInputType), {
1016-
name: inflection.inputType(
1017-
gqlTypeByTypeIdAndModifier[type.id][typeModifierKey]
1018-
),
1044+
baseInputType;
1045+
shouldSkipAddType = true;
1046+
} else {
1047+
const baseType = getGqlTypeByTypeIdAndModifier(
1048+
type.domainBaseTypeId,
1049+
typeModifier
1050+
);
1051+
const baseInputType = getGqlInputTypeByTypeIdAndModifier(
1052+
type.domainBaseTypeId,
1053+
typeModifier
1054+
);
1055+
// Hack stolen from: https://github.com/graphile/postgraphile/blob/ade728ed8f8e3ecdc5fdad7d770c67aa573578eb/src/graphql/schema/type/aliasGqlType.ts#L16
1056+
gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] =
1057+
Object.assign(Object.create(baseType), {
1058+
name: inflection.domainType(type),
10191059
description: type.description,
10201060
});
1061+
if (baseInputType && baseInputType !== baseType) {
1062+
gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey] =
1063+
Object.assign(Object.create(baseInputType), {
1064+
name: inflection.inputType(
1065+
gqlTypeByTypeIdAndModifier[type.id][typeModifierKey]
1066+
),
1067+
description: type.description,
1068+
});
1069+
}
10211070
}
10221071
}
10231072

@@ -1076,9 +1125,13 @@ end`;
10761125
gqlTypeByTypeIdAndModifier[type.id][typeModifierKey];
10771126
}
10781127
}
1079-
addType(
1080-
getNamedType(gqlTypeByTypeIdAndModifier[type.id][typeModifierKey])
1081-
);
1128+
// if its a domain type substituting for an enum table,
1129+
// adding it creates a duplicate
1130+
if (!shouldSkipAddType) {
1131+
addType(
1132+
getNamedType(gqlTypeByTypeIdAndModifier[type.id][typeModifierKey])
1133+
);
1134+
}
10821135
return gqlTypeByTypeIdAndModifier[type.id][typeModifierKey];
10831136
};
10841137

packages/postgraphile-core/__tests__/kitchen-sink-data.sql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,12 @@ insert into ranges.range_test(
321321

322322
alter sequence c.issue756_id_seq restart with 1;
323323
alter sequence inheritence.file_id_seq restart with 1;
324+
325+
--------------------------------------------------------------------------------
326+
327+
insert into function_returning_enum.applicants(
328+
id, first_name, last_name, stage, favorite_pet, transportation
329+
) values
330+
(1, 'John', 'Doe', 'round 2', 'CAT', 'BIKE' ),
331+
(2, 'David', 'Bowie', 'round 2', 'DOG', 'SUBWAY'),
332+
(3, 'John', 'Lennon', 'round 1', 'CAT', 'CAR');

packages/postgraphile-core/__tests__/kitchen-sink-schema.sql

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ drop schema if exists
1414
network_types,
1515
named_query_builder,
1616
enum_tables,
17-
geometry
17+
geometry,
18+
function_returning_enum
1819
cascade;
1920
drop extension if exists tablefunc;
2021
drop extension if exists intarray;
@@ -1238,3 +1239,125 @@ create table geometry.geom (
12381239
polygon polygon,
12391240
circle circle
12401241
);
1242+
1243+
--------------------------------------------------------------------------------
1244+
1245+
create schema function_returning_enum;
1246+
1247+
create table function_returning_enum.stage_options (
1248+
type text primary key
1249+
);
1250+
comment on table function_returning_enum.stage_options is E'@enum';
1251+
insert into function_returning_enum.stage_options (type) values ('pending'), ('round 1'), ('round 2'), ('rejected'), ('hired');
1252+
1253+
create type function_returning_enum.animal_type as enum (
1254+
'CAT',
1255+
'DOG',
1256+
'FISH'
1257+
);
1258+
1259+
1260+
create table function_returning_enum.enum_table (
1261+
transportation text not null constraint transportation_enum unique
1262+
);
1263+
1264+
comment on constraint transportation_enum on function_returning_enum.enum_table is E'@enum';
1265+
insert into function_returning_enum.enum_table (transportation) values ('CAR'), ('BIKE'), ('SUBWAY');
1266+
1267+
create table function_returning_enum.applicants (
1268+
id int,
1269+
first_name text,
1270+
last_name text,
1271+
favorite_pet function_returning_enum.animal_type,
1272+
stage text references function_returning_enum.stage_options (type),
1273+
transportation text references function_returning_enum.enum_table (transportation)
1274+
);
1275+
1276+
--- follow the convention of [enum_name]_enum_domain
1277+
create domain function_returning_enum.stage_options_enum_domain as text;
1278+
1279+
create function function_returning_enum.applicants_next_stage(
1280+
a function_returning_enum.applicants
1281+
) returns function_returning_enum.stage_options_enum_domain
1282+
as $$
1283+
select (case when a.stage = 'round 2' then 'hired'
1284+
else 'rejected' end)::function_returning_enum.stage_options_enum_domain;
1285+
$$ language sql stable;
1286+
comment on function function_returning_enum.applicants_next_stage is E'@filterable';
1287+
1288+
create table function_returning_enum.length_status (
1289+
type text primary key
1290+
);
1291+
comment on table function_returning_enum.length_status is E'@enum';
1292+
insert into function_returning_enum.length_status (type) values ('ok'), ('too_short');
1293+
1294+
--- use an @enum tag this time
1295+
create domain function_returning_enum.length as text;
1296+
comment on domain function_returning_enum.length is E'@enum length_status';
1297+
1298+
create function function_returning_enum.text_length(
1299+
text text,
1300+
min_length int
1301+
) returns function_returning_enum.length
1302+
as $$
1303+
select (case when length(text) < min_length then 'too_short'
1304+
else 'ok' end)::function_returning_enum.length;
1305+
$$ language sql stable;
1306+
1307+
create function function_returning_enum.applicants_name_length(
1308+
a function_returning_enum.applicants
1309+
) returns function_returning_enum.length
1310+
as $$
1311+
select function_returning_enum.text_length(a.last_name, 4)::function_returning_enum.length;
1312+
$$ language sql stable;
1313+
comment on function function_returning_enum.applicants_name_length is E'@filterable';
1314+
1315+
create function function_returning_enum.applicants_by_stage(
1316+
wanted_stage function_returning_enum.stage_options_enum_domain
1317+
) returns setof function_returning_enum.applicants
1318+
as $$
1319+
select * from function_returning_enum.applicants a where a.stage = wanted_stage;
1320+
$$ language sql stable;
1321+
1322+
create function function_returning_enum.applicants_by_favorite_pet(
1323+
pet function_returning_enum.animal_type
1324+
) returns setof function_returning_enum.applicants
1325+
as $$
1326+
select * from function_returning_enum.applicants a where a.favorite_pet = pet;
1327+
$$ language sql stable;
1328+
1329+
create function function_returning_enum.applicants_pet_food(
1330+
a function_returning_enum.applicants
1331+
) returns function_returning_enum.animal_type
1332+
as $$
1333+
select (case
1334+
when a.favorite_pet = 'FISH' then null
1335+
when a.favorite_pet = 'CAT' then 'FISH'
1336+
when a.favorite_pet = 'DOG' then 'CAT'
1337+
else null
1338+
end)::function_returning_enum.animal_type;
1339+
$$ language sql stable;
1340+
comment on function function_returning_enum.applicants_pet_food is E'@filterable';
1341+
1342+
create domain function_returning_enum.transportation as text;
1343+
comment on domain function_returning_enum.transportation is E'@enum enum_table_transportation_enum';
1344+
1345+
create function function_returning_enum.applicants_by_transportation(
1346+
transportation function_returning_enum.transportation
1347+
) returns setof function_returning_enum.applicants
1348+
as $$
1349+
select * from function_returning_enum.applicants a where a.transportation = applicants_by_transportation.transportation;
1350+
$$ language sql stable;
1351+
1352+
create function function_returning_enum.applicants_favorite_pet_transportation(
1353+
a function_returning_enum.applicants
1354+
) returns function_returning_enum.transportation
1355+
as $$
1356+
select (case
1357+
when a.favorite_pet = 'FISH' then 'SUBWAY'
1358+
when a.favorite_pet = 'CAT' then 'CAR'
1359+
when a.favorite_pet = 'DOG' then 'BIKE'
1360+
else null
1361+
end)::function_returning_enum.transportation;
1362+
$$ language sql stable;
1363+
comment on function function_returning_enum.applicants_favorite_pet_transportation is E'@filterable';

0 commit comments

Comments
 (0)