From 419ec87b5a8affd1d2c24c55b47f3252cd9e6b7f Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Tue, 4 Aug 2020 17:37:25 +0100 Subject: [PATCH] feat: add support for geometric types (#637) Opt in with: ```js const options = { graphileBuildOptions: { pgGeometricTypes: true, }, }; ``` --- .../src/plugins/PgTypesPlugin.js | 337 +++++++++++++ .../mutations/geometry.mutations.graphql | 104 ++++ .../fixtures/queries/geometry.queries.graphql | 68 +++ .../__snapshots__/mutations.test.js.snap | 115 +++++ .../__snapshots__/queries.test.js.snap | 105 ++++ .../__tests__/integration/mutations.test.js | 10 + .../__tests__/integration/queries.test.js | 10 + .../__snapshots__/geometry.test.js.snap | 475 ++++++++++++++++++ .../integration/schema/geometry.test.js | 10 + .../__tests__/kitchen-sink-data.sql | 16 + .../__tests__/kitchen-sink-schema.sql | 18 +- 11 files changed, 1267 insertions(+), 1 deletion(-) create mode 100644 packages/postgraphile-core/__tests__/fixtures/mutations/geometry.mutations.graphql create mode 100644 packages/postgraphile-core/__tests__/fixtures/queries/geometry.queries.graphql create mode 100644 packages/postgraphile-core/__tests__/integration/schema/__snapshots__/geometry.test.js.snap create mode 100644 packages/postgraphile-core/__tests__/integration/schema/geometry.test.js diff --git a/packages/graphile-build-pg/src/plugins/PgTypesPlugin.js b/packages/graphile-build-pg/src/plugins/PgTypesPlugin.js index c9d8b980c..d31ccac48 100644 --- a/packages/graphile-build-pg/src/plugins/PgTypesPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgTypesPlugin.js @@ -31,6 +31,7 @@ export default (function PgTypesPlugin( pgExtendedTypes = true, // Adding hstore support is technically a breaking change; this allows people to opt out easily: pgSkipHstore = false, + pgGeometricTypes = false, pgUseCustomNetworkScalars = false, disableIssue390Fix = false, } @@ -1147,6 +1148,342 @@ end`; ["PgTypes"] ); /* End of hstore type */ + + /* Geometric types */ + builder.hook( + "build", + build => { + // This hook tells graphile-build-pg about the hstore database type so it + // knows how to express it in input/output. + if (!pgGeometricTypes) return build; + const { + pgRegisterGqlTypeByTypeId, + pgRegisterGqlInputTypeByTypeId, + pgGetGqlTypeByTypeIdAndModifier, + pgGetGqlInputTypeByTypeIdAndModifier, + pg2GqlMapper, + pgSql: sql, + graphql: { + GraphQLObjectType, + GraphQLInputObjectType, + GraphQLList, + GraphQLBoolean, + GraphQLFloat, + }, + inflection, + } = build; + + // Check we have the hstore extension + const LINE = 628; + const LSEG = 601; + const BOX = 603; + const PATH = 602; + const POLYGON = 604; + const CIRCLE = 718; + + pgRegisterGqlTypeByTypeId(LINE, () => { + const Point = pgGetGqlTypeByTypeIdAndModifier("600", null); + if (!Point) { + throw new Error("Need point type"); + } + return new GraphQLObjectType({ + name: inflection.builtin("Line"), + description: + "An infinite line that passes through points 'a' and 'b'.", + fields: { + a: { type: Point }, + b: { type: Point }, + }, + }); + }); + pgRegisterGqlInputTypeByTypeId(LINE, () => { + const PointInput = pgGetGqlInputTypeByTypeIdAndModifier("600", null); + return new GraphQLInputObjectType({ + name: inflection.builtin("LineInput"), + description: + "An infinite line that passes through points 'a' and 'b'.", + fields: { + a: { type: PointInput }, + b: { type: PointInput }, + }, + }); + }); + pg2GqlMapper[LINE] = { + map: f => { + if (f[0] === "{" && f[f.length - 1] === "}") { + const [A, B, C] = f + .substr(1, f.length - 2) + .split(",") + .map(f => parseFloat(f)); + // Lines have the form Ax + By + C = 0. + // So if y = 0, Ax + C = 0; x = -C/A. + // If x = 0, By + C = 0; y = -C/B. + return { + a: { x: -C / A, y: 0 }, + b: { x: 0, y: -C / B }, + }; + } + }, + unmap: o => + sql.fragment`line(point(${sql.value(o.a.x)}, ${sql.value( + o.a.y + )}), point(${sql.value(o.b.x)}, ${sql.value(o.b.y)}))`, + }; + + pgRegisterGqlTypeByTypeId(LSEG, () => { + const Point = pgGetGqlTypeByTypeIdAndModifier("600", null); + return new GraphQLObjectType({ + name: inflection.builtin("LineSegment"), + description: "An finite line between points 'a' and 'b'.", + fields: { + a: { type: Point }, + b: { type: Point }, + }, + }); + }); + pgRegisterGqlInputTypeByTypeId(LSEG, () => { + const PointInput = pgGetGqlInputTypeByTypeIdAndModifier("600", null); + return new GraphQLInputObjectType({ + name: inflection.builtin("LineSegmentInput"), + description: "An finite line between points 'a' and 'b'.", + fields: { + a: { type: PointInput }, + b: { type: PointInput }, + }, + }); + }); + pg2GqlMapper[LSEG] = { + map: f => { + if (f[0] === "[" && f[f.length - 1] === "]") { + const [x1, y1, x2, y2] = f + .substr(1, f.length - 2) + .replace(/[()]/g, "") + .split(",") + .map(f => parseFloat(f)); + return { + a: { x: x1, y: y1 }, + b: { x: x2, y: y2 }, + }; + } + }, + unmap: o => + sql.fragment`lseg(point(${sql.value(o.a.x)}, ${sql.value( + o.a.y + )}), point(${sql.value(o.b.x)}, ${sql.value(o.b.y)}))`, + }; + + pgRegisterGqlTypeByTypeId(BOX, () => { + const Point = pgGetGqlTypeByTypeIdAndModifier("600", null); + return new GraphQLObjectType({ + name: inflection.builtin("Box"), + description: + "A rectangular box defined by two opposite corners 'a' and 'b'", + fields: { + a: { type: Point }, + b: { type: Point }, + }, + }); + }); + pgRegisterGqlInputTypeByTypeId(BOX, () => { + const PointInput = pgGetGqlInputTypeByTypeIdAndModifier("600", null); + return new GraphQLInputObjectType({ + name: inflection.builtin("BoxInput"), + description: + "A rectangular box defined by two opposite corners 'a' and 'b'", + fields: { + a: { type: PointInput }, + b: { type: PointInput }, + }, + }); + }); + pg2GqlMapper[BOX] = { + map: f => { + if (f[0] === "(" && f[f.length - 1] === ")") { + const [x1, y1, x2, y2] = f + .substr(1, f.length - 2) + .replace(/[()]/g, "") + .split(",") + .map(f => parseFloat(f)); + return { + a: { x: x1, y: y1 }, + b: { x: x2, y: y2 }, + }; + } + }, + unmap: o => + sql.fragment`box(point(${sql.value(o.a.x)}, ${sql.value( + o.a.y + )}), point(${sql.value(o.b.x)}, ${sql.value(o.b.y)}))`, + }; + + pgRegisterGqlTypeByTypeId(PATH, () => { + const Point = pgGetGqlTypeByTypeIdAndModifier("600", null); + return new GraphQLObjectType({ + name: inflection.builtin("Path"), + description: "A path (open or closed) made up of points", + fields: { + points: { + type: new GraphQLList(Point), + }, + isOpen: { + description: + "True if this is a closed path (similar to a polygon), false otherwise.", + type: GraphQLBoolean, + }, + }, + }); + }); + pgRegisterGqlInputTypeByTypeId(PATH, () => { + const PointInput = pgGetGqlInputTypeByTypeIdAndModifier("600", null); + return new GraphQLInputObjectType({ + name: inflection.builtin("PathInput"), + description: "A path (open or closed) made up of points", + fields: { + points: { + type: new GraphQLList(PointInput), + }, + isOpen: { + description: + "True if this is a closed path (similar to a polygon), false otherwise.", + type: GraphQLBoolean, + }, + }, + }); + }); + pg2GqlMapper[PATH] = { + map: f => { + let isOpen = null; + if (f[0] === "(" && f[f.length - 1] === ")") { + isOpen = false; + } else if (f[0] === "[" && f[f.length - 1] === "]") { + isOpen = true; + } + if (isOpen !== null) { + const xsAndYs = f + .substr(1, f.length - 2) + .replace(/[()]/g, "") + .split(",") + .map(f => parseFloat(f)); + if (xsAndYs.length % 2 !== 0) { + throw new Error("Invalid path representation"); + } + const points = []; + for (let i = 0, l = xsAndYs.length; i < l; i += 2) { + points.push({ x: xsAndYs[i], y: xsAndYs[i + 1] }); + } + return { + isOpen, + points, + }; + } + }, + unmap: o => { + const openParen = o.isOpen ? "[" : "("; + const closeParen = o.isOpen ? "]" : ")"; + const val = `${openParen}${o.points + .map(p => `(${p.x},${p.y})`) + .join(",")}${closeParen}`; + return sql.value(val); + }, + }; + + pgRegisterGqlTypeByTypeId(POLYGON, () => { + const Point = pgGetGqlTypeByTypeIdAndModifier("600", null); + return new GraphQLObjectType({ + name: inflection.builtin("Polygon"), + fields: { + points: { + type: new GraphQLList(Point), + }, + }, + }); + }); + pgRegisterGqlInputTypeByTypeId(POLYGON, () => { + const PointInput = pgGetGqlInputTypeByTypeIdAndModifier("600", null); + return new GraphQLInputObjectType({ + name: inflection.builtin("PolygonInput"), + fields: { + points: { + type: new GraphQLList(PointInput), + }, + }, + }); + }); + pg2GqlMapper[POLYGON] = { + map: f => { + if (f[0] === "(" && f[f.length - 1] === ")") { + const xsAndYs = f + .substr(1, f.length - 2) + .replace(/[()]/g, "") + .split(",") + .map(f => parseFloat(f)); + if (xsAndYs.length % 2 !== 0) { + throw new Error("Invalid polygon representation"); + } + const points = []; + for (let i = 0, l = xsAndYs.length; i < l; i += 2) { + points.push({ x: xsAndYs[i], y: xsAndYs[i + 1] }); + } + return { + points, + }; + } + }, + unmap: o => { + const val = `(${o.points.map(p => `(${p.x},${p.y})`).join(",")})`; + return sql.value(val); + }, + }; + + pgRegisterGqlTypeByTypeId(CIRCLE, () => { + const Point = pgGetGqlTypeByTypeIdAndModifier("600", null); + return new GraphQLObjectType({ + name: inflection.builtin("Circle"), + fields: { + center: { type: Point }, + radius: { type: GraphQLFloat }, + }, + }); + }); + pgRegisterGqlInputTypeByTypeId(CIRCLE, () => { + const PointInput = pgGetGqlInputTypeByTypeIdAndModifier("600", null); + return new GraphQLInputObjectType({ + name: inflection.builtin("CircleInput"), + fields: { + center: { type: PointInput }, + radius: { type: GraphQLFloat }, + }, + }); + }); + pg2GqlMapper[CIRCLE] = { + map: f => { + if (f[0] === "<" && f[f.length - 1] === ">") { + const [x, y, r] = f + .substr(1, f.length - 2) + .replace(/[()]/g, "") + .split(",") + .map(f => parseFloat(f)); + return { + center: { x, y }, + radius: r, + }; + } + }, + unmap: o => + sql.fragment`circle(point(${sql.value(o.center.x)}, ${sql.value( + o.center.y + )}), ${sql.value(o.radius)})`, + }; + + // TODO: add the non-nulls! + + return build; + }, + ["PgGeometryTypes"], + [], + ["PgTypes"] + ); + /* End of geometric types */ }: Plugin); function makeGraphQLHstoreType(graphql, hstoreTypeName) { diff --git a/packages/postgraphile-core/__tests__/fixtures/mutations/geometry.mutations.graphql b/packages/postgraphile-core/__tests__/fixtures/mutations/geometry.mutations.graphql new file mode 100644 index 000000000..9e2eb36b3 --- /dev/null +++ b/packages/postgraphile-core/__tests__/fixtures/mutations/geometry.mutations.graphql @@ -0,0 +1,104 @@ +mutation { + createGeom( + input: { + geom: { + point: { x: 99, y: 1234 } + line: { a: { x: 99, y: 1234 }, b: { x: 0, y: 111 } } + lseg: { a: { x: 99, y: 111 }, b: { x: 2935, y: 3548 } } + box: { a: { x: 123, y: 52635 }, b: { x: 2342, y: 12445 } } + openPath: { + isOpen: true + points: [ + { x: 0, y: 0 } + { x: 0, y: 10 } + { x: 10, y: 10 } + { x: 10, y: 0 } + ] + } + closedPath: { + isOpen: false + points: [ + { x: 0, y: 0 } + { x: 0, y: 10 } + { x: 10, y: 10 } + { x: 10, y: 0 } + ] + } + polygon: { + points: [ + { x: 0, y: 0 } + { x: 0, y: 10 } + { x: 10, y: 10 } + { x: 10, y: 0 } + ] + } + circle: { center: { x: 7, y: 11 }, radius: 3 } + } + } + ) { + geom { + id + point { + x + y + } + line { + a { + x + y + } + b { + x + y + } + } + lseg { + a { + x + y + } + b { + x + y + } + } + box { + a { + x + y + } + b { + x + y + } + } + openPath { + isOpen + points { + x + y + } + } + closedPath { + isOpen + points { + x + y + } + } + polygon { + points { + x + y + } + } + circle { + center { + x + y + } + radius + } + } + } +} diff --git a/packages/postgraphile-core/__tests__/fixtures/queries/geometry.queries.graphql b/packages/postgraphile-core/__tests__/fixtures/queries/geometry.queries.graphql new file mode 100644 index 000000000..2c9d85bc4 --- /dev/null +++ b/packages/postgraphile-core/__tests__/fixtures/queries/geometry.queries.graphql @@ -0,0 +1,68 @@ +{ + allGeoms { + nodes { + id + point { + x + y + } + line { + a { + x + y + } + b { + x + y + } + } + lseg { + a { + x + y + } + b { + x + y + } + } + box { + a { + x + y + } + b { + x + y + } + } + openPath { + isOpen + points { + x + y + } + } + closedPath { + isOpen + points { + x + y + } + } + polygon { + points { + x + y + } + } + circle { + center { + x + y + } + radius + } + } + } +} 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 a9044ec86..fec65dc20 100644 --- a/packages/postgraphile-core/__tests__/integration/__snapshots__/mutations.test.js.snap +++ b/packages/postgraphile-core/__tests__/integration/__snapshots__/mutations.test.js.snap @@ -80,6 +80,121 @@ Object { } `; +exports[`geometry.mutations.graphql 1`] = ` +Object { + "data": Object { + "createGeom": Object { + "geom": Object { + "box": Object { + "a": Object { + "x": 2342, + "y": 52635, + }, + "b": Object { + "x": 123, + "y": 12445, + }, + }, + "circle": Object { + "center": Object { + "x": 7, + "y": 11, + }, + "radius": 3, + }, + "closedPath": Object { + "isOpen": false, + "points": Array [ + Object { + "x": 0, + "y": 0, + }, + Object { + "x": 0, + "y": 10, + }, + Object { + "x": 10, + "y": 10, + }, + Object { + "x": 10, + "y": 0, + }, + ], + }, + "id": 102, + "line": Object { + "a": Object { + "x": -9.785396260017848, + "y": 0, + }, + "b": Object { + "x": 0, + "y": 111, + }, + }, + "lseg": Object { + "a": Object { + "x": 99, + "y": 111, + }, + "b": Object { + "x": 2935, + "y": 3548, + }, + }, + "openPath": Object { + "isOpen": true, + "points": Array [ + Object { + "x": 0, + "y": 0, + }, + Object { + "x": 0, + "y": 10, + }, + Object { + "x": 10, + "y": 10, + }, + Object { + "x": 10, + "y": 0, + }, + ], + }, + "point": Object { + "x": 99, + "y": 1234, + }, + "polygon": Object { + "points": Array [ + Object { + "x": 0, + "y": 0, + }, + Object { + "x": 0, + "y": 10, + }, + Object { + "x": 10, + "y": 10, + }, + Object { + "x": 10, + "y": 0, + }, + ], + }, + }, + }, + }, +} +`; + exports[`inheritence.createUserFile.graphql 1`] = ` Object { "data": Object { 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 e491f170a..acfa3831e 100644 --- a/packages/postgraphile-core/__tests__/integration/__snapshots__/queries.test.js.snap +++ b/packages/postgraphile-core/__tests__/integration/__snapshots__/queries.test.js.snap @@ -1908,6 +1908,111 @@ Object { } `; +exports[`geometry.queries.graphql 1`] = ` +Object { + "data": Object { + "allGeoms": Object { + "nodes": Array [ + Object { + "box": Object { + "a": Object { + "x": 13, + "y": 17, + }, + "b": Object { + "x": 7, + "y": 11, + }, + }, + "circle": Object { + "center": Object { + "x": 10, + "y": 10, + }, + "radius": 7, + }, + "closedPath": Object { + "isOpen": false, + "points": Array [ + Object { + "x": 1, + "y": 3, + }, + Object { + "x": 3, + "y": 4, + }, + Object { + "x": 4, + "y": 1, + }, + ], + }, + "id": 101, + "line": Object { + "a": Object { + "x": -4, + "y": 0, + }, + "b": Object { + "x": 0, + "y": 4, + }, + }, + "lseg": Object { + "a": Object { + "x": 7, + "y": 11, + }, + "b": Object { + "x": 13, + "y": 17, + }, + }, + "openPath": Object { + "isOpen": true, + "points": Array [ + Object { + "x": 1, + "y": 3, + }, + Object { + "x": 3, + "y": 4, + }, + Object { + "x": 4, + "y": 1, + }, + ], + }, + "point": Object { + "x": 4, + "y": 2, + }, + "polygon": Object { + "points": Array [ + Object { + "x": 1, + "y": 3, + }, + Object { + "x": 3, + "y": 4, + }, + Object { + "x": 4, + "y": 1, + }, + ], + }, + }, + ], + }, + }, +} +`; + exports[`json-overflow.graphql 1`] = ` Object { "data": Object { diff --git a/packages/postgraphile-core/__tests__/integration/mutations.test.js b/packages/postgraphile-core/__tests__/integration/mutations.test.js index 8de245c09..2cbded47b 100644 --- a/packages/postgraphile-core/__tests__/integration/mutations.test.js +++ b/packages/postgraphile-core/__tests__/integration/mutations.test.js @@ -42,6 +42,7 @@ beforeAll(() => { useCustomNetworkScalarsSchema, pg10UseCustomNetworkScalarsSchema, enumTables, + geometry, ] = await Promise.all([ createPostGraphileSchema(pgClient, ["a", "b", "c"]), createPostGraphileSchema(pgClient, ["d"]), @@ -62,6 +63,11 @@ beforeAll(() => { }) : null, createPostGraphileSchema(pgClient, ["enum_tables"]), + createPostGraphileSchema(pgClient, ["geometry"], { + graphileBuildOptions: { + pgGeometricTypes: true, + }, + }), ]); // Now for RBAC-enabled tests await pgClient.query("set role postgraphile_test_authenticator"); @@ -77,6 +83,7 @@ beforeAll(() => { pg10UseCustomNetworkScalarsSchema, rbacSchema, enumTables, + geometry, }; }); @@ -97,6 +104,7 @@ beforeAll(() => { pg10UseCustomNetworkScalarsSchema, rbacSchema, enumTables, + geometry, } = await gqlSchemaPromise; // Get a new Postgres client and run the mutation. return await withPgClient(async pgClient => { @@ -121,6 +129,8 @@ beforeAll(() => { schemaToUse = inheritenceSchema; } else if (fileName.startsWith("enum_tables.")) { schemaToUse = enumTables; + } else if (fileName.startsWith("geometry.")) { + schemaToUse = geometry; } else if (fileName.startsWith("pg10.")) { if (serverVersionNum < 100000) { // eslint-disable-next-line diff --git a/packages/postgraphile-core/__tests__/integration/queries.test.js b/packages/postgraphile-core/__tests__/integration/queries.test.js index c10f8bb80..98d90fb68 100644 --- a/packages/postgraphile-core/__tests__/integration/queries.test.js +++ b/packages/postgraphile-core/__tests__/integration/queries.test.js @@ -60,6 +60,7 @@ beforeAll(() => { pg10UseCustomNetworkScalars, namedQueryBuilder, enumTables, + geometry, ] = await Promise.all([ createPostGraphileSchema(pgClient, ["a", "b", "c"], { subscriptions: true, @@ -129,6 +130,12 @@ beforeAll(() => { createPostGraphileSchema(pgClient, ["enum_tables"], { subscriptions: true, }), + createPostGraphileSchema(pgClient, ["geometry"], { + subscriptions: true, + graphileBuildOptions: { + pgGeometricTypes: true, + }, + }), ]); // Now for RBAC-enabled tests await pgClient.query("set role postgraphile_test_authenticator"); @@ -154,6 +161,7 @@ beforeAll(() => { pg10UseCustomNetworkScalars, namedQueryBuilder, enumTables, + geometry, }; }); @@ -228,6 +236,8 @@ beforeAll(() => { gqlSchema = gqlSchemas.namedQueryBuilder; } else if (fileName.startsWith("enum_tables.")) { gqlSchema = gqlSchemas.enumTables; + } else if (fileName.startsWith("geometry.")) { + gqlSchema = gqlSchemas.geometry; } else { gqlSchema = gqlSchemas.normal; } diff --git a/packages/postgraphile-core/__tests__/integration/schema/__snapshots__/geometry.test.js.snap b/packages/postgraphile-core/__tests__/integration/schema/__snapshots__/geometry.test.js.snap new file mode 100644 index 000000000..46e449d0e --- /dev/null +++ b/packages/postgraphile-core/__tests__/integration/schema/__snapshots__/geometry.test.js.snap @@ -0,0 +1,475 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`prints a schema for geometry 1`] = ` +"""A rectangular box defined by two opposite corners 'a' and 'b'""" +type Box { + a: Point + b: Point +} + +"""A rectangular box defined by two opposite corners 'a' and 'b'""" +input BoxInput { + a: PointInput + b: PointInput +} + +type Circle { + center: Point + radius: Float +} + +input CircleInput { + center: PointInput + radius: Float +} + +"""All input for the create \`Geom\` mutation.""" +input CreateGeomInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + + """The \`Geom\` to be created by this mutation.""" + geom: GeomInput! +} + +"""The output of our create \`Geom\` mutation.""" +type CreateGeomPayload { + """ + The exact same \`clientMutationId\` that was provided in the mutation input, + unchanged and unused. May be used by a client to track mutations. + """ + clientMutationId: String + + """The \`Geom\` that was created by this mutation.""" + geom: Geom + + """An edge for our \`Geom\`. May be used by Relay 1.""" + geomEdge( + """The method to use when ordering \`Geom\`.""" + orderBy: [GeomsOrderBy!] = [PRIMARY_KEY_ASC] + ): GeomsEdge + + """ + Our root query field type. Allows us to run any query from our mutation payload. + """ + query: Query +} + +"""A location in a connection that can be used for resuming pagination.""" +scalar Cursor + +"""All input for the \`deleteGeomById\` mutation.""" +input DeleteGeomByIdInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + id: Int! +} + +"""All input for the \`deleteGeom\` mutation.""" +input DeleteGeomInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + + """ + The globally unique \`ID\` which will identify a single \`Geom\` to be deleted. + """ + nodeId: ID! +} + +"""The output of our delete \`Geom\` mutation.""" +type DeleteGeomPayload { + """ + The exact same \`clientMutationId\` that was provided in the mutation input, + unchanged and unused. May be used by a client to track mutations. + """ + clientMutationId: String + deletedGeomId: ID + + """The \`Geom\` that was deleted by this mutation.""" + geom: Geom + + """An edge for our \`Geom\`. May be used by Relay 1.""" + geomEdge( + """The method to use when ordering \`Geom\`.""" + orderBy: [GeomsOrderBy!] = [PRIMARY_KEY_ASC] + ): GeomsEdge + + """ + Our root query field type. Allows us to run any query from our mutation payload. + """ + query: Query +} + +type Geom implements Node { + box: Box + circle: Circle + closedPath: Path + id: Int! + line: Line + lseg: LineSegment + + """ + A globally unique identifier. Can be used in various places throughout the system to identify this single value. + """ + nodeId: ID! + openPath: Path + point: Point + polygon: Polygon +} + +""" +A condition to be used against \`Geom\` object types. All fields are tested for equality and combined with a logical ‘and.’ +""" +input GeomCondition { + """Checks for equality with the object’s \`box\` field.""" + box: BoxInput + + """Checks for equality with the object’s \`circle\` field.""" + circle: CircleInput + + """Checks for equality with the object’s \`closedPath\` field.""" + closedPath: PathInput + + """Checks for equality with the object’s \`id\` field.""" + id: Int + + """Checks for equality with the object’s \`line\` field.""" + line: LineInput + + """Checks for equality with the object’s \`lseg\` field.""" + lseg: LineSegmentInput + + """Checks for equality with the object’s \`openPath\` field.""" + openPath: PathInput + + """Checks for equality with the object’s \`point\` field.""" + point: PointInput + + """Checks for equality with the object’s \`polygon\` field.""" + polygon: PolygonInput +} + +"""An input for mutations affecting \`Geom\`""" +input GeomInput { + box: BoxInput + circle: CircleInput + closedPath: PathInput + id: Int + line: LineInput + lseg: LineSegmentInput + openPath: PathInput + point: PointInput + polygon: PolygonInput +} + +"""Represents an update to a \`Geom\`. Fields that are set will be updated.""" +input GeomPatch { + box: BoxInput + circle: CircleInput + closedPath: PathInput + id: Int + line: LineInput + lseg: LineSegmentInput + openPath: PathInput + point: PointInput + polygon: PolygonInput +} + +"""A connection to a list of \`Geom\` values.""" +type GeomsConnection { + """ + A list of edges which contains the \`Geom\` and cursor to aid in pagination. + """ + edges: [GeomsEdge!]! + + """A list of \`Geom\` objects.""" + nodes: [Geom]! + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """The count of *all* \`Geom\` you could get from the connection.""" + totalCount: Int! +} + +"""A \`Geom\` edge in the connection.""" +type GeomsEdge { + """A cursor for use in pagination.""" + cursor: Cursor + + """The \`Geom\` at the end of the edge.""" + node: Geom +} + +"""Methods to use when ordering \`Geom\`.""" +enum GeomsOrderBy { + BOX_ASC + BOX_DESC + CIRCLE_ASC + CIRCLE_DESC + CLOSED_PATH_ASC + CLOSED_PATH_DESC + ID_ASC + ID_DESC + LINE_ASC + LINE_DESC + LSEG_ASC + LSEG_DESC + NATURAL + OPEN_PATH_ASC + OPEN_PATH_DESC + POINT_ASC + POINT_DESC + POLYGON_ASC + POLYGON_DESC + PRIMARY_KEY_ASC + PRIMARY_KEY_DESC +} + +"""An infinite line that passes through points 'a' and 'b'.""" +type Line { + a: Point + b: Point +} + +"""An infinite line that passes through points 'a' and 'b'.""" +input LineInput { + a: PointInput + b: PointInput +} + +"""An finite line between points 'a' and 'b'.""" +type LineSegment { + a: Point + b: Point +} + +"""An finite line between points 'a' and 'b'.""" +input LineSegmentInput { + a: PointInput + b: PointInput +} + +""" +The root mutation type which contains root level fields which mutate data. +""" +type Mutation { + """Creates a single \`Geom\`.""" + createGeom( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: CreateGeomInput! + ): CreateGeomPayload + + """Deletes a single \`Geom\` using its globally unique id.""" + deleteGeom( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: DeleteGeomInput! + ): DeleteGeomPayload + + """Deletes a single \`Geom\` using a unique key.""" + deleteGeomById( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: DeleteGeomByIdInput! + ): DeleteGeomPayload + + """Updates a single \`Geom\` using its globally unique id and a patch.""" + updateGeom( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: UpdateGeomInput! + ): UpdateGeomPayload + + """Updates a single \`Geom\` using a unique key and a patch.""" + updateGeomById( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: UpdateGeomByIdInput! + ): UpdateGeomPayload +} + +"""An object with a globally unique \`ID\`.""" +interface Node { + """ + A globally unique identifier. Can be used in various places throughout the system to identify this single value. + """ + nodeId: ID! +} + +"""Information about pagination in a connection.""" +type PageInfo { + """When paginating forwards, the cursor to continue.""" + endCursor: Cursor + + """When paginating forwards, are there more items?""" + hasNextPage: Boolean! + + """When paginating backwards, are there more items?""" + hasPreviousPage: Boolean! + + """When paginating backwards, the cursor to continue.""" + startCursor: Cursor +} + +"""A path (open or closed) made up of points""" +type Path { + """True if this is a closed path (similar to a polygon), false otherwise.""" + isOpen: Boolean + points: [Point] +} + +"""A path (open or closed) made up of points""" +input PathInput { + """True if this is a closed path (similar to a polygon), false otherwise.""" + isOpen: Boolean + points: [PointInput] +} + +type Point { + x: Float! + y: Float! +} + +input PointInput { + x: Float! + y: Float! +} + +type Polygon { + points: [Point] +} + +input PolygonInput { + points: [PointInput] +} + +"""The root query type which gives access points into the data universe.""" +type Query implements Node { + """Reads and enables pagination through a set of \`Geom\`.""" + allGeoms( + """Read all values in the set after (below) this cursor.""" + after: Cursor + + """Read all values in the set before (above) this cursor.""" + before: Cursor + + """ + A condition to be used in determining which values should be returned by the collection. + """ + condition: GeomCondition + + """Only read the first \`n\` values of the set.""" + first: Int + + """Only read the last \`n\` values of the set.""" + last: Int + + """ + Skip the first \`n\` values from our \`after\` cursor, an alternative to cursor + based pagination. May not be used with \`last\`. + """ + offset: Int + + """The method to use when ordering \`Geom\`.""" + orderBy: [GeomsOrderBy!] = [PRIMARY_KEY_ASC] + ): GeomsConnection + + """Reads a single \`Geom\` using its globally unique \`ID\`.""" + geom( + """The globally unique \`ID\` to be used in selecting a single \`Geom\`.""" + nodeId: ID! + ): Geom + geomById(id: Int!): Geom + + """Fetches an object given its globally unique \`ID\`.""" + node( + """The globally unique \`ID\`.""" + nodeId: ID! + ): Node + + """ + The root query type must be a \`Node\` to work well with Relay 1 mutations. This just resolves to \`query\`. + """ + nodeId: ID! + + """ + Exposes the root query type nested one level down. This is helpful for Relay 1 + which can only query top level fields if they are in a particular form. + """ + query: Query! +} + +"""All input for the \`updateGeomById\` mutation.""" +input UpdateGeomByIdInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + + """ + An object where the defined keys will be set on the \`Geom\` being updated. + """ + geomPatch: GeomPatch! + id: Int! +} + +"""All input for the \`updateGeom\` mutation.""" +input UpdateGeomInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + + """ + An object where the defined keys will be set on the \`Geom\` being updated. + """ + geomPatch: GeomPatch! + + """ + The globally unique \`ID\` which will identify a single \`Geom\` to be updated. + """ + nodeId: ID! +} + +"""The output of our update \`Geom\` mutation.""" +type UpdateGeomPayload { + """ + The exact same \`clientMutationId\` that was provided in the mutation input, + unchanged and unused. May be used by a client to track mutations. + """ + clientMutationId: String + + """The \`Geom\` that was updated by this mutation.""" + geom: Geom + + """An edge for our \`Geom\`. May be used by Relay 1.""" + geomEdge( + """The method to use when ordering \`Geom\`.""" + orderBy: [GeomsOrderBy!] = [PRIMARY_KEY_ASC] + ): GeomsEdge + + """ + Our root query field type. Allows us to run any query from our mutation payload. + """ + query: Query +} + +`; diff --git a/packages/postgraphile-core/__tests__/integration/schema/geometry.test.js b/packages/postgraphile-core/__tests__/integration/schema/geometry.test.js new file mode 100644 index 000000000..12778c335 --- /dev/null +++ b/packages/postgraphile-core/__tests__/integration/schema/geometry.test.js @@ -0,0 +1,10 @@ +const core = require("./core"); + +test( + "prints a schema for geometry", + core.test(["geometry"], { + graphileBuildOptions: { + pgGeometricTypes: true, + }, + }) +); diff --git a/packages/postgraphile-core/__tests__/kitchen-sink-data.sql b/packages/postgraphile-core/__tests__/kitchen-sink-data.sql index 405950bbd..eeace825f 100644 --- a/packages/postgraphile-core/__tests__/kitchen-sink-data.sql +++ b/packages/postgraphile-core/__tests__/kitchen-sink-data.sql @@ -258,3 +258,19 @@ insert into enum_tables.letter_descriptions(letter, description) values ('B', 'Following closely behind the first letter, this is a popular choice'), ('C', 'Pronounced like ''sea'''), ('D', 'The first letter omitted from the ''ABC'' phrase'); + +-------------------------------------------------------------------------------- + +alter sequence geometry.geom_id_seq restart with 101; +insert into geometry.geom( + point, line, lseg, box, open_path, closed_path, polygon, circle +) values ( + point(4, 2), + line(point(7, 11), point(13, 17)), + lseg(point(7, 11), point(13, 17)), + box(point(7, 11), point(13, 17)), + '[(1,3),(3,4),(4,1)]', + '((1,3),(3,4),(4,1))', + '((1,3),(3,4),(4,1))', + '<(10, 10), 7>' +); diff --git a/packages/postgraphile-core/__tests__/kitchen-sink-schema.sql b/packages/postgraphile-core/__tests__/kitchen-sink-schema.sql index fef6df8cc..ca64f17b2 100644 --- a/packages/postgraphile-core/__tests__/kitchen-sink-schema.sql +++ b/packages/postgraphile-core/__tests__/kitchen-sink-schema.sql @@ -13,7 +13,8 @@ drop schema if exists large_bigint, network_types, named_query_builder, - enum_tables + enum_tables, + geometry cascade; drop extension if exists tablefunc; drop extension if exists intarray; @@ -1127,3 +1128,18 @@ create table enum_tables.letter_descriptions( letter text not null references enum_tables.abcd unique, description text ); + +-------------------------------------------------------------------------------- + +create schema geometry; +create table geometry.geom ( + id serial primary key, + point point, + line line, + lseg lseg, + box box, + open_path path, + closed_path path, + polygon polygon, + circle circle +);