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
6 changes: 6 additions & 0 deletions .changeset/odd-tools-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@dataplan/pg": minor
"postgraphile": minor
---

Empty ranges are now recognized and represented both on input and output.
8 changes: 8 additions & 0 deletions .changeset/shaggy-mugs-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"graphile-build-pg": patch
"postgraphile": patch
"@dataplan/pg": patch
---

Tweak some types to allow defining the types of the final PgRegistry via
declaration merging (requires codegen).
96 changes: 57 additions & 39 deletions grafast/dataplan-pg/src/codecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -869,10 +869,25 @@ function escapeRangeValue<
return `"${encoded.replace(/"/g, '""')}"`;
}

interface PgRange<T> {
start: { value: T; inclusive: boolean } | null;
end: { value: T; inclusive: boolean } | null;
}
/** The value of a 'range' type from Postgres */
export type PgRangeValue<T> =
| { empty: true }
| {
empty: false;
/** The lower bound; inclusive or exclusive. If null, there is no lower bound */
start: { value: T; inclusive: boolean } | null;
/** The upper bound; inclusive or exclusive. If null, there is no upper bound */
end: { value: T; inclusive: boolean } | null;
};

export type PgCodecFromPostgresType<TCodec extends PgCodec> =
TCodec extends PgCodec<any, any, infer TValue, any, any, any, any>
? TValue
: never;
export type PgCodecFromJavaScriptType<TCodec extends PgCodec> =
TCodec extends PgCodec<any, any, any, infer TValue, any, any, any>
? TValue
: never;

/**
* Returns a PgCodec that represents a range of the given inner PgCodec
Expand Down Expand Up @@ -908,7 +923,7 @@ export function rangeOfCodec<
TName,
undefined,
string,
PgRange<unknown>,
PgRangeValue<PgCodecFromJavaScriptType<TInnerCodec>>,
undefined,
undefined,
TInnerCodec
Expand All @@ -925,7 +940,7 @@ export function rangeOfCodec<
)},\n${innerCodec.castFromPg!(
sql`upper(${frag})`,
innerCodec.notNull,
)},\nupper_inc(${frag})`,
)},\nupper_inc(${frag}),\nisempty(${frag})`,
)})::text`;
}
: null;
Expand All @@ -946,44 +961,47 @@ export function rangeOfCodec<
: null),
fromPg: needsCast
? function (value) {
const json = JSON.parse(value);
return {
start:
json[1] != null
? {
value: innerCodec.fromPg(json[1]),
inclusive: !!json[0],
}
: null,
end:
json[2] != null
? {
value: innerCodec.fromPg(json[2]),
inclusive: !!json[3],
}
: null,
};
const [lowerInc, lower, upper, upperInc, empty] = JSON.parse(value);
return empty
? { empty: true }
: {
empty: false,
start:
lower != null
? { value: innerCodec.fromPg(lower), inclusive: !!lowerInc }
: null,
end:
upper != null
? { value: innerCodec.fromPg(upper), inclusive: !!upperInc }
: null,
};
}
: function (value) {
const parsed = rangeParse(value);
return {
start:
parsed.lower != null
? {
value: innerCodec.fromPg(parsed.lower),
inclusive: parsed.isLowerBoundClosed(),
}
: null,
end:
parsed.upper != null
? {
value: innerCodec.fromPg(parsed.upper),
inclusive: parsed.isUpperBoundClosed(),
}
: null,
};
return parsed.isEmpty()
? { empty: true }
: {
empty: false,
start:
parsed.lower != null
? {
value: innerCodec.fromPg(parsed.lower),
inclusive: parsed.isLowerBoundClosed(),
}
: null,
end:
parsed.upper != null
? {
value: innerCodec.fromPg(parsed.upper),
inclusive: parsed.isUpperBoundClosed(),
}
: null,
};
},
toPg(value) {
if (value.empty) {
return "empty";
}
let str = "";
if (value.start == null) {
str += "(";
Expand Down
5 changes: 4 additions & 1 deletion grafast/dataplan-pg/src/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,14 @@ export interface PgResourceParameter<
*/
export interface PgResourceUnique<
TAttributes extends PgCodecAttributes = PgCodecAttributes,
TUniqueAttributes extends ReadonlyArray<
keyof TAttributes & string
> = ReadonlyArray<keyof TAttributes & string>,
> {
/**
* The attributes that are unique
*/
attributes: ReadonlyArray<keyof TAttributes & string>;
attributes: TUniqueAttributes;
/**
* If this is true, this represents the "primary key" of the resource.
*/
Expand Down
6 changes: 6 additions & 0 deletions grafast/dataplan-pg/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import type {
PgCodecAttributes,
PgCodecAttributeVia,
PgCodecAttributeViaExplicit,
PgCodecFromJavaScriptType,
PgCodecFromPostgresType,
PgEnumCodecSpec,
PgRangeValue,
PgRecordTypeCodecSpec,
} from "./codecs.ts";
import {
Expand Down Expand Up @@ -233,6 +236,8 @@ export type {
PgCodecAttributeVia,
PgCodecAttributeViaExplicit,
PgCodecExtensions,
PgCodecFromJavaScriptType,
PgCodecFromPostgresType,
PgCodecList,
PgCodecPolymorphism,
PgCodecPolymorphismRelational,
Expand Down Expand Up @@ -278,6 +283,7 @@ export type {
PgPath,
PgPoint,
PgPolygon,
PgRangeValue,
PgRecordTypeCodecSpec,
PgRefDefinition,
PgRefDefinitionExtensions,
Expand Down
25 changes: 3 additions & 22 deletions grafast/dataplan-pg/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,10 +462,7 @@ export type PlanByUniques<
TAttributes extends PgCodecAttributes,
TUniqueAttributes extends ReadonlyArray<PgResourceUnique<TAttributes>>,
> = TAttributes extends PgCodecAttributes
? TuplePlanMap<
TAttributes,
TUniqueAttributes[number]["attributes"] & string[]
>[number]
? TuplePlanMap<TAttributes, TUniqueAttributes[number]["attributes"]>[number]
: undefined;

export type PgConditionLike = Modifier<any> & {
Expand Down Expand Up @@ -652,12 +649,7 @@ export interface PgRegistry<
PgCodec<string, PgCodecAttributes | undefined, any, any, any, any, any>
>,
TResourceOptions extends {
[name in string]: PgResourceOptions<
name,
PgCodec, // TCodecs[keyof TCodecs],
ReadonlyArray<PgResourceUnique<PgCodecAttributes>>,
readonly PgResourceParameter[] | undefined
>;
[name in string]: PgResourceOptions<any, any, any, any>;
} = Record<
string,
PgResourceOptions<
Expand All @@ -670,18 +662,7 @@ export interface PgRegistry<
>,
TRelations extends {
[codecName in keyof TCodecs]?: {
[relationName in string]: PgCodecRelationConfig<
// TCodecs[keyof TCodecs] &
PgCodec<string, PgCodecAttributes, any, any, undefined, any, undefined>,
// TResourceOptions[keyof TResourceOptions] &
PgResourceOptions<
any,
// TCodecs[keyof TCodecs] &
PgCodecWithAttributes,
any,
any
>
>;
[relationName in string]: PgCodecRelationConfig<any, any>;
};
} = Record<
string,
Expand Down
9 changes: 8 additions & 1 deletion graphile-build/graphile-build-pg/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,15 @@ declare global {
[tagName: string]: null | true | string | (string | true)[];
}

/** Augment this interface to provide generated build-time types. */
interface GeneratedTypes {}

interface BuildInput {
pgRegistry: PgRegistry;
pgRegistry: GeneratedTypes extends {
pgRegistry: infer TRegistry extends PgRegistry;
}
? TRegistry
: PgRegistry;
}

interface SchemaOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type {
PgCodec,
PgCodecRelation,
PgCodecWithAttributes,
PgExecutor,
PgRefDefinition,
PgResource,
PgResourceUnique,
Expand Down Expand Up @@ -142,8 +141,13 @@ declare global {
/**
* Shortcut to primary executor; equivalent for most users to `build.input.pgRegistry.pgExecutors.main`.
* (strictly it's `build.input.pgRegistry.pgExecutors[Object.keys(build.input.pgRegistry.pgExecutors)[0]]`)
*
* Note: the TypeScript type for this is an approximation, because
* TypeScript doesn't have a concept of "first key". For most people
* there will be just a single executor anyway, and the type of the
* executor doesn't tend to matter much.
*/
pgExecutor: PgExecutor;
pgExecutor: GraphileBuild.Build["input"]["pgRegistry"]["pgExecutors"][keyof GraphileBuild.Build["input"]["pgRegistry"]["pgExecutors"]];
/** Shortcut to the resources in the registry */
pgResources: GraphileBuild.Build["input"]["pgRegistry"]["pgResources"];
/** Shortcut to the codecs in the registry */
Expand Down
25 changes: 25 additions & 0 deletions graphile-build/graphile-build-pg/src/plugins/PgCodecsPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1342,6 +1342,19 @@ export const PgCodecsPlugin: GraphileConfig.Plugin = {
"type",
),
fields: () => ({
// We should have done this as a union type but a)
// object types require at least one field, and what
// field would an empty range have? b) we already
// shipped the other fields...
empty: {
description: build.wrapDescription(
"If the range has no start or end, it is either unbounded (`false`) or empty (`true`).",
"field",
),
type: new build.graphql.GraphQLNonNull(
build.graphql.GraphQLBoolean,
),
},
start: {
description: build.wrapDescription(
"The starting bound of our range.",
Expand Down Expand Up @@ -1374,7 +1387,19 @@ export const PgCodecsPlugin: GraphileConfig.Plugin = {
`A range of \`${underlyingInputTypeName}\`.`,
"type",
),

fields: () => ({
// This should have been a `@oneOf` input type, with `empty` as one type... but too late now.
empty: {
description: build.wrapDescription(
"If `true`, the range is seen as empty and `start`/`end` are ignored. If `false` (default), setting `start` and `end` to `null` (or omitting them) indicates an unbounded range.",
"field",
),
type: new build.graphql.GraphQLNonNull(
build.graphql.GraphQLBoolean,
),
defaultValue: false,
},
start: {
description: build.wrapDescription(
"The starting bound of our range.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
a: {
clientMutationId: null,
type: {
nodeId: "WyJ0eXBlcyIsMjAxXQ==",
id: 201,
numrange: {
empty: true,
start: null,
end: null,
},
daterange: {
empty: true,
start: null,
end: null,
},
anIntRange: {
empty: false,
start: null,
end: {
value: 500,
inclusive: false,
},
},
},
query: {
nodeId: "query",
},
},
}
Loading
Loading