From d2c1e95f8fadb1b30bd960a757056ecc7b1e1f6c Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Sun, 3 Dec 2017 09:26:33 +0000 Subject: [PATCH] fix(types): Prevent numeric overflow for arbitrary-precision types (#123) * Prevent numeric types from overflowing * Update tests --- .../src/plugins/PgTypesPlugin.js | 14 +- .../mutations/mutation-create.graphql | 6 +- .../__snapshots__/mutations.test.js.snap | 6 +- .../__snapshots__/queries.test.js.snap | 8 +- .../__snapshots__/schema.test.js.snap | 190 ++++++++++-------- .../__tests__/kitchen-sink-schema.sql | 6 +- 6 files changed, 131 insertions(+), 99 deletions(-) diff --git a/packages/graphile-build-pg/src/plugins/PgTypesPlugin.js b/packages/graphile-build-pg/src/plugins/PgTypesPlugin.js index 6d328b76e..f43673e6f 100644 --- a/packages/graphile-build-pg/src/plugins/PgTypesPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgTypesPlugin.js @@ -231,7 +231,11 @@ export default (function PgTypesPlugin( }, {}); const categoryLookup = { B: () => GraphQLBoolean, - N: () => GraphQLFloat, + + // Numbers may be too large for GraphQL/JS to handle, so stringify by + // default. + N: () => GraphQLString, + A: type => new GraphQLList( enforceGqlTypeByPgType(pgTypeById[type.arrayItemTypeId]) @@ -315,16 +319,24 @@ export default (function PgTypesPlugin( ), // bitint - even though this is int8, it's too big for JS int, so cast to string. "21": GraphQLInt, // int2 "23": GraphQLInt, // int4 + "700": GraphQLFloat, // float4 + "701": GraphQLFloat, // float8 + "1700": GraphQLString, // numeric "790": GraphQLFloat, // money + "1186": GQLInterval, // interval "1082": SimpleDate, // date "1114": SimpleDatetime, // timestamp "1184": SimpleDatetime, // timestamptz "1083": SimpleTime, // time "1266": SimpleTime, // timetz + "114": SimpleJSON, // json "3802": SimpleJSON, // jsonb "2950": SimpleUUID, // uuid + + "1560": GraphQLString, // bit + "1562": GraphQLString, // varbit }, pgExtendedTypes && { "114": GraphQLJSON, diff --git a/packages/postgraphile-core/__tests__/fixtures/mutations/mutation-create.graphql b/packages/postgraphile-core/__tests__/fixtures/mutations/mutation-create.graphql index 723f7fa54..a083ab136 100644 --- a/packages/postgraphile-core/__tests__/fixtures/mutations/mutation-create.graphql +++ b/packages/postgraphile-core/__tests__/fixtures/mutations/mutation-create.graphql @@ -4,8 +4,8 @@ mutation { id: 201 smallint: 30 bigint: "467131188225" - numeric: 15.2 - decimal: 15.2 + numeric: "15.2" + decimal: "15.2" boolean: false varchar: "abc" enum: RED @@ -16,7 +16,7 @@ mutation { json: "{\"x\":1,\"y\":2,\"z\":3}" jsonb: "{\"a\":1,\"b\":2,\"c\":3}" numrange: { - start: { value: 50, inclusive: true } + start: { value: "50", inclusive: true } } daterange: { start: { value: "1927-11-05", inclusive: false } diff --git a/packages/postgraphile-core/__tests__/integration/__snapshots__/mutations.test.js.snap b/packages/postgraphile-core/__tests__/integration/__snapshots__/mutations.test.js.snap index 02ca34b30..c5141b49d 100644 --- a/packages/postgraphile-core/__tests__/integration/__snapshots__/mutations.test.js.snap +++ b/packages/postgraphile-core/__tests__/integration/__snapshots__/mutations.test.js.snap @@ -38,7 +38,7 @@ Object { "value": "1927-11-06", }, }, - "decimal": 15.2, + "decimal": "15.2", "domain": 6, "domain2": 5, "enum": "RED", @@ -80,12 +80,12 @@ Object { "bazBuz": 0, }, "nodeId": "WyJ0eXBlcyIsMjAxXQ==", - "numeric": 15.2, + "numeric": "15.2", "numrange": Object { "end": null, "start": Object { "inclusive": true, - "value": 50, + "value": "50", }, }, "smallint": 30, diff --git a/packages/postgraphile-core/__tests__/integration/__snapshots__/queries.test.js.snap b/packages/postgraphile-core/__tests__/integration/__snapshots__/queries.test.js.snap index 699df8adb..7c79cf4b8 100644 --- a/packages/postgraphile-core/__tests__/integration/__snapshots__/queries.test.js.snap +++ b/packages/postgraphile-core/__tests__/integration/__snapshots__/queries.test.js.snap @@ -2475,11 +2475,11 @@ Object { "numrange": Object { "end": Object { "inclusive": false, - "value": 52, + "value": "52", }, "start": Object { "inclusive": true, - "value": -10, + "value": "-10", }, }, "smallint": 50, @@ -2550,11 +2550,11 @@ Object { "numrange": Object { "end": Object { "inclusive": false, - "value": 52, + "value": "52", }, "start": Object { "inclusive": true, - "value": -10, + "value": "-10", }, }, "smallint": 50, diff --git a/packages/postgraphile-core/__tests__/integration/__snapshots__/schema.test.js.snap b/packages/postgraphile-core/__tests__/integration/__snapshots__/schema.test.js.snap index 4faa4194b..60789424a 100644 --- a/packages/postgraphile-core/__tests__/integration/__snapshots__/schema.test.js.snap +++ b/packages/postgraphile-core/__tests__/integration/__snapshots__/schema.test.js.snap @@ -1560,42 +1560,6 @@ enum EnumWithEmptyString { TWO } -# A range of \`Float\`. -type FloatRange { - # The ending bound of our range. - end: FloatRangeBound - - # The starting bound of our range. - start: FloatRangeBound -} - -# The value at one end of a range. A range can either include this value, or not. -type FloatRangeBound { - # Whether or not the value of this bound is included in the range. - inclusive: Boolean! - - # The value at one end of our range. - value: Float! -} - -# The value at one end of a range. A range can either include this value, or not. -input FloatRangeBoundInput { - # Whether or not the value of this bound is included in the range. - inclusive: Boolean! - - # The value at one end of our range. - value: Float! -} - -# A range of \`Float\`. -input FloatRangeInput { - # The ending bound of our range. - end: FloatRangeBoundInput - - # The starting bound of our range. - start: FloatRangeBoundInput -} - scalar Guid # All input for the \`guidFn\` mutation. @@ -1942,6 +1906,42 @@ type Query implements Node { typeById(id: Int!): Type } +# A range of \`String\`. +type StringRange { + # The ending bound of our range. + end: StringRangeBound + + # The starting bound of our range. + start: StringRangeBound +} + +# The value at one end of a range. A range can either include this value, or not. +type StringRangeBound { + # Whether or not the value of this bound is included in the range. + inclusive: Boolean! + + # The value at one end of our range. + value: String! +} + +# The value at one end of a range. A range can either include this value, or not. +input StringRangeBoundInput { + # Whether or not the value of this bound is included in the range. + inclusive: Boolean! + + # The value at one end of our range. + value: String! +} + +# A range of \`String\`. +input StringRangeInput { + # The ending bound of our range. + end: StringRangeBoundInput + + # The starting bound of our range. + start: StringRangeBoundInput +} + # The exact time of day, does not include the date. May or may not have a timezone offset. scalar Time @@ -1952,7 +1952,7 @@ type Type implements Node { compoundType: CompoundType! date: Date! daterange: DateRange! - decimal: Float! + decimal: String! domain: AnInt! domain2: AnotherInt! enum: Color! @@ -1966,9 +1966,9 @@ type Type implements Node { # A globally unique identifier. Can be used in various places throughout the system to identify this single value. nodeId: ID! - nullableRange: FloatRange - numeric: Float! - numrange: FloatRange! + nullableRange: StringRange + numeric: String! + numrange: StringRange! smallint: Int! textArray: [String]! time: Time! @@ -1999,7 +1999,7 @@ input TypeCondition { daterange: DateRangeInput # Checks for equality with the object’s \`decimal\` field. - decimal: Float + decimal: String # Checks for equality with the object’s \`domain\` field. domain: AnInt @@ -2032,13 +2032,13 @@ input TypeCondition { nestedCompoundType: NestedCompoundTypeInput # Checks for equality with the object’s \`nullableRange\` field. - nullableRange: FloatRangeInput + nullableRange: StringRangeInput # Checks for equality with the object’s \`numeric\` field. - numeric: Float + numeric: String # Checks for equality with the object’s \`numrange\` field. - numrange: FloatRangeInput + numrange: StringRangeInput # Checks for equality with the object’s \`smallint\` field. smallint: Int @@ -2070,7 +2070,7 @@ input TypeInput { compoundType: CompoundTypeInput! date: Date! daterange: DateRangeInput! - decimal: Float! + decimal: String! domain: AnInt! domain2: AnotherInt! enum: Color! @@ -2081,9 +2081,9 @@ input TypeInput { jsonb: JSON! money: Float! nestedCompoundType: NestedCompoundTypeInput! - nullableRange: FloatRangeInput - numeric: Float! - numrange: FloatRangeInput! + nullableRange: StringRangeInput + numeric: String! + numrange: StringRangeInput! smallint: Int! textArray: [String]! time: Time! @@ -2101,7 +2101,7 @@ input TypePatch { compoundType: CompoundTypeInput date: Date daterange: DateRangeInput - decimal: Float + decimal: String domain: AnInt domain2: AnotherInt enum: Color @@ -2112,9 +2112,9 @@ input TypePatch { jsonb: JSON money: Float nestedCompoundType: NestedCompoundTypeInput - nullableRange: FloatRangeInput - numeric: Float - numrange: FloatRangeInput + nullableRange: StringRangeInput + numeric: String + numrange: StringRangeInput smallint: Int textArray: [String] time: Time @@ -3607,24 +3607,6 @@ enum EnumWithEmptyString { TWO } -# A range of \`Float\`. -type FloatRange { - # The ending bound of our range. - end: FloatRangeBound - - # The starting bound of our range. - start: FloatRangeBound -} - -# The value at one end of a range. A range can either include this value, or not. -type FloatRangeBound { - # Whether or not the value of this bound is included in the range. - inclusive: Boolean! - - # The value at one end of our range. - value: Float! -} - # The value at one end of a range. A range can either include this value, or not. input FloatRangeBoundInput { # Whether or not the value of this bound is included in the range. @@ -5586,6 +5568,42 @@ enum SimilarTable2SOrderBy { PRIMARY_KEY_DESC } +# A range of \`String\`. +type StringRange { + # The ending bound of our range. + end: StringRangeBound + + # The starting bound of our range. + start: StringRangeBound +} + +# The value at one end of a range. A range can either include this value, or not. +type StringRangeBound { + # Whether or not the value of this bound is included in the range. + inclusive: Boolean! + + # The value at one end of our range. + value: String! +} + +# The value at one end of a range. A range can either include this value, or not. +input StringRangeBoundInput { + # Whether or not the value of this bound is included in the range. + inclusive: Boolean! + + # The value at one end of our range. + value: String! +} + +# A range of \`String\`. +input StringRangeInput { + # The ending bound of our range. + end: StringRangeBoundInput + + # The starting bound of our range. + start: StringRangeBoundInput +} + type TablefuncCrosstab2 { category1: String category2: String @@ -5802,7 +5820,7 @@ type Type implements Node { compoundType: CompoundType! date: Date! daterange: DateRange! - decimal: Float! + decimal: String! domain: AnInt! domain2: AnotherInt! enum: Color! @@ -5816,9 +5834,9 @@ type Type implements Node { # A globally unique identifier. Can be used in various places throughout the system to identify this single value. nodeId: ID! - nullableRange: FloatRange - numeric: Float! - numrange: FloatRange! + nullableRange: StringRange + numeric: String! + numrange: StringRange! smallint: Int! textArray: [String]! time: Time! @@ -5849,7 +5867,7 @@ input TypeCondition { daterange: DateRangeInput # Checks for equality with the object’s \`decimal\` field. - decimal: Float + decimal: String # Checks for equality with the object’s \`domain\` field. domain: AnInt @@ -5882,13 +5900,13 @@ input TypeCondition { nestedCompoundType: NestedCompoundTypeInput # Checks for equality with the object’s \`nullableRange\` field. - nullableRange: FloatRangeInput + nullableRange: StringRangeInput # Checks for equality with the object’s \`numeric\` field. - numeric: Float + numeric: String # Checks for equality with the object’s \`numrange\` field. - numrange: FloatRangeInput + numrange: StringRangeInput # Checks for equality with the object’s \`smallint\` field. smallint: Int @@ -5920,7 +5938,7 @@ input TypeInput { compoundType: CompoundTypeInput! date: Date! daterange: DateRangeInput! - decimal: Float! + decimal: String! domain: AnInt! domain2: AnotherInt! enum: Color! @@ -5931,9 +5949,9 @@ input TypeInput { jsonb: JSON! money: Float! nestedCompoundType: NestedCompoundTypeInput! - nullableRange: FloatRangeInput - numeric: Float! - numrange: FloatRangeInput! + nullableRange: StringRangeInput + numeric: String! + numrange: StringRangeInput! smallint: Int! textArray: [String]! time: Time! @@ -5951,7 +5969,7 @@ input TypePatch { compoundType: CompoundTypeInput date: Date daterange: DateRangeInput - decimal: Float + decimal: String domain: AnInt domain2: AnotherInt enum: Color @@ -5962,9 +5980,9 @@ input TypePatch { jsonb: JSON money: Float nestedCompoundType: NestedCompoundTypeInput - nullableRange: FloatRangeInput - numeric: Float - numrange: FloatRangeInput + nullableRange: StringRangeInput + numeric: String + numrange: StringRangeInput smallint: Int textArray: [String] time: Time diff --git a/packages/postgraphile-core/__tests__/kitchen-sink-schema.sql b/packages/postgraphile-core/__tests__/kitchen-sink-schema.sql index cf673bfdc..dd02eaa8d 100644 --- a/packages/postgraphile-core/__tests__/kitchen-sink-schema.sql +++ b/packages/postgraphile-core/__tests__/kitchen-sink-schema.sql @@ -81,6 +81,8 @@ create type b.nested_compound_type as ( baz_buz int ); +create type c.floatrange as range (subtype = float8, subtype_diff = float8mi); + comment on type c.compound_type is 'Awesome feature!'; create view b.updatable_view as @@ -201,8 +203,8 @@ create function c.jsonb_identity(json jsonb) returns jsonb as $$ select json $$ create function c.jsonb_identity_mutation(json jsonb) returns jsonb as $$ select json $$ language sql; create function c.jsonb_identity_mutation_plpgsql(_the_json jsonb) returns jsonb as $$ declare begin return _the_json; end; $$ language plpgsql strict security definer; create function c.jsonb_identity_mutation_plpgsql_with_default(_the_json jsonb default '[]') returns jsonb as $$ declare begin return _the_json; end; $$ language plpgsql strict security definer; -create function c.types_query(a bigint, b boolean, c varchar, d integer[], e json, f numrange) returns boolean as $$ select false $$ language sql stable strict; -create function c.types_mutation(a bigint, b boolean, c varchar, d integer[], e json, f numrange) returns boolean as $$ select false $$ language sql strict; +create function c.types_query(a bigint, b boolean, c varchar, d integer[], e json, f c.floatrange) returns boolean as $$ select false $$ language sql stable strict; +create function c.types_mutation(a bigint, b boolean, c varchar, d integer[], e json, f c.floatrange) returns boolean as $$ select false $$ language sql strict; create function b.compound_type_query(object c.compound_type) returns c.compound_type as $$ select (object.a + 1, object.b, object.c, object.d, object.e, object.f, object.foo_bar)::c.compound_type $$ language sql stable; create function c.compound_type_set_query() returns setof c.compound_type as $$ select (1, '2', 'blue', null, '0_BAR', '', 7)::c.compound_type $$ language sql stable; create function b.compound_type_mutation(object c.compound_type) returns c.compound_type as $$ select (object.a + 1, object.b, object.c, object.d, object.e, object.f, object.foo_bar)::c.compound_type $$ language sql;