Computed columns returning a domain over a composite type are not exposed
- Dominant language
- TypeScript
- Stars
- 12.9k
- Forks
- 625
- Avg merge
- 5h 23m
- Merged PRs (30d)
- 24
Description
### Summary
PostgreSQL computed columns are dropped from the GraphQL schema when its return type is a domain over a composite type. No error, the field just doesn't appear. The same function returning the underlying composite
directly works. Domain columns of the same domain type work fine.
### Steps to reproduce
Schema:
```sql
create type money_raw as (amount numeric(10,2), currency_code text);
create domain money as money_raw check (length((value).currency_code) = 3);
create table thing (
id serial primary key,
net_amount money,
tax_amount money
);
create function thing_total_amount(t thing) returns money as $$
select case
when t.net_amount is null or t.tax_amount is null then null::money
else row((t.net_amount).amount + (t.tax_amount).amount,
(t.net_amount).currency_code)::money
end
$$ language sql stable;
create function thing_total_amount_raw(t thing) returns money_raw as $$
select row((t.net_amount).amount + (t.tax_amount).amount,
(t.net_amount).currency_code)::money_raw
$$ language sql stable;
```
Build the schema and inspect the `Thing` type.
### Expected results
`Thing.totalAmount: Money` — the computed column should be exposed using the same GraphQL output type (`Money`) that the domain columns already use.
### Actual results
- `thing_total_amount` (returns the domain `money`) → no `totalAmount` field at all.
- `thing_total_amount_raw` (returns the composite `money_raw`) → field appears, typed as a separate `MoneyRaw` object type.
- Scalars and domain-over-scalar return types work as expected.
### Additional context
- `postgraphile@5.0.3`, `graphile-build-pg@5.0.2`, `@dataplan/pg@1.0.3`, `grafast@1.0.2`, `graphile-build@5.0.2`, `graphql@16.14.0`
- Node 22, schema built via `makeSchema`
- No custom behavior tags involved
Root cause (traced in `graphile-build-pg@5.0.2/dist/plugins/PgProceduresPlugin.js`):
`@dataplan/pg`'s `domainOfCodec` builds the domain codec by spreading the inner composite codec (`...innerCodec`), so the domain codec inherits `.attributes` from `money_raw` (and additionally carries `domainOfCodec`). In `PgProceduresPlugin`, the resource-building branch is gated only on `returnCodec.attributes || returnCodec.arrayOfCodec?.attributes`, so the domain-over-composite return is routed into the composite/table-return branch. That branch resolves the backing `pg_class` from the return type's `typrelid`:
```js
const pgClass = await info.helpers.pgIntrospection.getClass(serviceName, pgType.typrelid);
if (!pgClass) return null; // resource silently dropped
```
A domain's `pg_type.typrelid` is `0`, so `getClass` returns null and the entire function resource is dropped. (For comparison: a bare composite has a real `typrelid`, so its resource builds — but it then registers/uses a separate `MoneyRaw` object type rather than the domain's `Money`.)
Domain columns are unaffected because `PgTablesPlugin` registers the `Money` output type for the domain codec (keyed by codec identity) and `PgAttributesPlugin` resolves columns through that registration.
### Possible Solution
The domain-over-composite return should not be treated as a table/composite return; it should keep its own (domain) codec, which already has a registered output type. Excluding domain codecs from the composite branch so they fall through to the scalar/`else` branch (which builds the resource with `codec: returnCodec`) fixes it, the field is then exposed using the domain's already-registered `Money` type, and no duplicate `MoneyRaw` type is created:
```diff
if (!returnCodec.isAnonymous &&
+ !returnCodec.domainOfCodec &&
(returnCodec.attributes || returnCodec.arrayOfCodec?.attributes)) {
```
Verified against `graphile-build-pg@5.0.2`: with this one-line change, a computed column returning a domain-over-composite is exposed using the same GraphQL type as the domain columns (`Money`), and the stray `MoneyRaw` type disappears.
Contributor guide
Assessment
This issue has not been assessed yet.