From 1e040ba6cef36cc166ac25e25c60d77e249bdd56 Mon Sep 17 00:00:00 2001 From: Matt Bretl Date: Sat, 8 Sep 2018 18:23:18 +0100 Subject: [PATCH] feat(pg): PG10 identity columns and preliminary PG11 support (#294) Fixes #244 Uses a template string to support multiple PostgreSQL versions. --- .../src/plugins/PgColumnsPlugin.js | 4 +- .../src/plugins/PgIntrospectionPlugin.d.ts | 1 + .../src/plugins/PgIntrospectionPlugin.js | 12 +- .../plugins/introspectionQuery.js} | 37 +- .../fixtures/mutations/pg10.identity.graphql | 33 + .../__snapshots__/mutations.test.js.snap | 25 + .../__tests__/integration/mutations.test.js | 23 +- .../schema/__snapshots__/pg10.test.js.snap | 645 ++++++++++++++++++ .../__tests__/integration/schema/pg10.test.js | 3 + packages/postgraphile-core/__tests__/pg10.sql | 12 + packages/postgraphile-core/scripts/test | 13 +- 11 files changed, 789 insertions(+), 19 deletions(-) rename packages/graphile-build-pg/{res/introspection-query.sql => src/plugins/introspectionQuery.js} (92%) create mode 100644 packages/postgraphile-core/__tests__/fixtures/mutations/pg10.identity.graphql create mode 100644 packages/postgraphile-core/__tests__/integration/schema/__snapshots__/pg10.test.js.snap create mode 100644 packages/postgraphile-core/__tests__/integration/schema/pg10.test.js create mode 100644 packages/postgraphile-core/__tests__/pg10.sql diff --git a/packages/graphile-build-pg/src/plugins/PgColumnsPlugin.js b/packages/graphile-build-pg/src/plugins/PgColumnsPlugin.js index e4f5cafd8..d31fcd4ef 100644 --- a/packages/graphile-build-pg/src/plugins/PgColumnsPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgColumnsPlugin.js @@ -223,6 +223,7 @@ export default (function PgColumnsPlugin(builder) { isPgBaseInput ? "base" : isPgPatch ? "update" : "create" ) ) + .filter(attr => attr.identity !== "a") .reduce((memo, attr) => { const fieldName = inflection.column(attr); if (memo[fieldName]) { @@ -248,7 +249,8 @@ export default (function PgColumnsPlugin(builder) { isPgBaseInput || isPgPatch || (!attr.isNotNull && !attr.type.domainIsNotNull) || - attr.hasDefault, + attr.hasDefault || + attr.identity === "d", pgGetGqlInputTypeByTypeIdAndModifier( attr.typeId, attr.typeModifier diff --git a/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.d.ts b/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.d.ts index 219a45e18..daf58df1c 100644 --- a/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.d.ts +++ b/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.d.ts @@ -82,6 +82,7 @@ export interface PgAttribute { typeModifier: number; isNotNull: boolean; hasDefault: boolean; + identity: "" | "a" | "d"; class: PgClass; type: PgType; namespace: PgNamespace; diff --git a/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.js b/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.js index 8c68f13b5..5d69395d4 100644 --- a/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.js @@ -9,11 +9,11 @@ import chalk from "chalk"; import throttle from "lodash/throttle"; import flatMap from "lodash/flatMap"; import { quacksLikePgPool } from "../withPgClient"; +import { makeIntrospectionQuery } from "./introspectionQuery"; import { version } from "../../package.json"; const debug = debugFactory("graphile-build-pg"); -const INTROSPECTION_PATH = `${__dirname}/../../res/introspection-query.sql`; const WATCH_FIXTURES_PATH = `${__dirname}/../../res/watch-fixtures.sql`; // Ref: https://github.com/graphile/postgraphile/tree/master/src/postgres/introspection/object @@ -102,6 +102,7 @@ export type PgAttribute = { typeModifier: number, isNotNull: boolean, hasDefault: boolean, + identity: "" | "a" | "d", class: PgClass, type: PgType, namespace: PgNamespace, @@ -175,7 +176,14 @@ export default (async function PgIntrospectionPlugin( const introspectionResultsByKind = cloneResults( await persistentMemoizeWithKey(cacheKey, () => withPgClient(pgConfig, async pgClient => { - const introspectionQuery = await readFile(INTROSPECTION_PATH, "utf8"); + const versionResult = await pgClient.query( + "show server_version_num;" + ); + const serverVersionNum = parseInt( + versionResult.rows[0].server_version_num, + 10 + ); + const introspectionQuery = makeIntrospectionQuery(serverVersionNum); const { rows } = await pgClient.query(introspectionQuery, [ schemas, pgIncludeExtensionResources, diff --git a/packages/graphile-build-pg/res/introspection-query.sql b/packages/graphile-build-pg/src/plugins/introspectionQuery.js similarity index 92% rename from packages/graphile-build-pg/res/introspection-query.sql rename to packages/graphile-build-pg/src/plugins/introspectionQuery.js index c4af70b3f..1d877f034 100644 --- a/packages/graphile-build-pg/res/introspection-query.sql +++ b/packages/graphile-build-pg/src/plugins/introspectionQuery.js @@ -1,9 +1,12 @@ +// @flow +function makeIntrospectionQuery(serverVersionNum: number): string { + return `\ -- @see https://www.postgresql.org/docs/9.5/static/catalogs.html -- @see https://github.com/graphile/postgraphile/blob/master/resources/introspection-query.sql -- -- ## Parameters --- - `$1`: An array of strings that represent the namespaces we are introspecting. --- - `$2`: set true to include functions/tables/etc that come from extensions +-- - \`$1\`: An array of strings that represent the namespaces we are introspecting. +-- - \`$2\`: set true to include functions/tables/etc that come from extensions with recursive accessible_roles(_oid) as ( select oid _oid, pg_roles.* @@ -66,10 +69,13 @@ with -- TODO: Variadic arguments. pro.provariadic = 0 and -- Filter our aggregate functions and window functions. - pro.proisagg = false and - pro.proiswindow = false and + ${ + serverVersionNum >= 110000 + ? "pro.prokind = 'f'" + : "pro.proisagg = false and pro.proiswindow = false" + } and -- We want to make sure the argument mode for all of our arguments is - -- `IN` which means `proargmodes` will be null. + -- \`IN\` which means \`proargmodes\` will be null. pro.proargmodes is null and -- Do not select procedures that create range types. These are utility -- functions that really don’t need to be exposed in an API. @@ -77,7 +83,7 @@ with -- Do not expose trigger functions (type trigger has oid 2279) pro.prorettype <> 2279 and -- We don't want functions that will clash with GraphQL (treat them as private) - pro.proname not like E'\\_\\_%' and + pro.proname not like E'\\\\_\\\\_%' and -- We also don’t want procedures that have been defined in our namespace -- twice. This leads to duplicate fields in the API which throws an -- error. In the future we may support this case. For now though, it is @@ -106,14 +112,14 @@ with nsp.nspname as "namespaceName", rel.reltype as "typeId", -- Here we determine whether or not we can use this class in a - -- `SELECT`’s `FROM` clause. In order to determine this we look at them - -- `relkind` column, if it is `i` (index) or `c` (composite), we cannot + -- \`SELECT\`’s \`FROM\` clause. In order to determine this we look at them + -- \`relkind\` column, if it is \`i\` (index) or \`c\` (composite), we cannot -- select this class. Otherwise we can. rel.relkind not in ('i', 'c') as "isSelectable", -- Here we are determining whether we can insert/update/delete a class. -- This is helpful as it lets us detect non-updatable views and then -- exclude them from being inserted/updated/deleted into. For more info - -- on how `pg_catalog.pg_relation_is_updatable` works: + -- on how \`pg_catalog.pg_relation_is_updatable\` works: -- -- - https://www.postgresql.org/message-id/CAEZATCV2_qN9P3zbvADwME_TkYf2gR_X2cLQR4R+pqkwxGxqJg@mail.gmail.com -- - https://github.com/postgres/postgres/blob/2410a2543e77983dab1f63f48b2adcd23dba994e/src/backend/utils/adt/misc.c#L684 @@ -132,7 +138,7 @@ with where rel.relpersistence in ('p') and -- We don't want classes that will clash with GraphQL (treat them as private) - rel.relname not like E'\\_\\_%' and + rel.relname not like E'\\\\_\\\\_%' and rel.relkind in ('r', 'v', 'm', 'c', 'f') and ($2 is true or not exists( select 1 @@ -157,6 +163,7 @@ with nullif(att.atttypmod, -1) as "typeModifier", att.attnotnull as "isNotNull", att.atthasdef as "hasDefault", + ${serverVersionNum >= 100000 ? "att.attidentity" : "''"} as "identity", exists(select 1 from accessible_roles where has_column_privilege(accessible_roles.oid, att.attrelid, att.attname, 'SELECT')) as "aclSelectable", exists(select 1 from accessible_roles where has_column_privilege(accessible_roles.oid, att.attrelid, att.attname, 'INSERT')) as "aclInsertable", exists(select 1 from accessible_roles where has_column_privilege(accessible_roles.oid, att.attrelid, att.attname, 'UPDATE')) as "aclUpdatable" @@ -167,14 +174,14 @@ with att.attrelid in (select "id" from class) and att.attnum > 0 and -- We don't want attributes that will clash with GraphQL (treat them as private) - att.attname not like E'\\_\\_%' and + att.attname not like E'\\\\_\\\\_%' and not att.attisdropped order by att.attrelid, att.attnum ), -- @see https://www.postgresql.org/docs/9.5/static/catalog-pg-type.html type as ( - -- Use another `WITH` statement here, because our `WHERE` clause will need + -- Use another \`WITH\` statement here, because our \`WHERE\` clause will need -- to use it. with type_all as ( select @@ -311,3 +318,9 @@ select row_to_json(x) as object from procedure as x union all select row_to_json(x) as object from extension as x ; +`; +} + +module.exports = { + makeIntrospectionQuery, +}; diff --git a/packages/postgraphile-core/__tests__/fixtures/mutations/pg10.identity.graphql b/packages/postgraphile-core/__tests__/fixtures/mutations/pg10.identity.graphql new file mode 100644 index 000000000..a000ea128 --- /dev/null +++ b/packages/postgraphile-core/__tests__/fixtures/mutations/pg10.identity.graphql @@ -0,0 +1,33 @@ +mutation { + a: createAlwaysAsIdentity(input:{ + alwaysAsIdentity: { + t: "test" + } + }) { + alwaysAsIdentity { + id + t + } + } + b: createByDefaultAsIdentity(input:{ + byDefaultAsIdentity: { + t: "test" + } + }) { + byDefaultAsIdentity { + id + t + } + } + c: createByDefaultAsIdentity(input:{ + byDefaultAsIdentity: { + id: 100 + t: "test" + } + }) { + byDefaultAsIdentity { + id + t + } + } +} 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 c28ffa646..a4cfc61ab 100644 --- a/packages/postgraphile-core/__tests__/integration/__snapshots__/mutations.test.js.snap +++ b/packages/postgraphile-core/__tests__/integration/__snapshots__/mutations.test.js.snap @@ -869,6 +869,31 @@ Object { } `; +exports[`pg10.identity.graphql 1`] = ` +Object { + "data": Object { + "a": Object { + "alwaysAsIdentity": Object { + "id": 1, + "t": "test", + }, + }, + "b": Object { + "byDefaultAsIdentity": Object { + "id": 1, + "t": "test", + }, + }, + "c": Object { + "byDefaultAsIdentity": Object { + "id": 100, + "t": "test", + }, + }, + }, +} +`; + exports[`procedure-mutation.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 e532a3dc2..51b46cb81 100644 --- a/packages/postgraphile-core/__tests__/integration/mutations.test.js +++ b/packages/postgraphile-core/__tests__/integration/mutations.test.js @@ -13,6 +13,11 @@ function readFile(filename, encoding) { }); } +async function getServerVersionNum(pgClient) { + const versionResult = await pgClient.query("show server_version_num;"); + return parseInt(versionResult.rows[0].server_version_num, 10); +} + // This test suite can be flaky. Increase it’s timeout. jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 20; @@ -31,10 +36,13 @@ beforeAll(() => { const gqlSchemaPromise = withPgClient(async pgClient => { // A selection of omit/rename comments on the d schema await pgClient.query(await dSchemaComments()); - - const [gqlSchema, dSchema] = await Promise.all([ + const serverVersionNum = await getServerVersionNum(pgClient); + const [gqlSchema, dSchema, pg10Schema] = await Promise.all([ createPostGraphileSchema(pgClient, ["a", "b", "c"]), createPostGraphileSchema(pgClient, ["d"]), + serverVersionNum >= 100000 + ? createPostGraphileSchema(pgClient, ["pg10"]) + : null, ]); // Now for RBAC-enabled tests await pgClient.query("set role postgraphile_test_authenticator"); @@ -44,6 +52,7 @@ beforeAll(() => { return { gqlSchema, dSchema, + pg10Schema, rbacSchema, }; }); @@ -56,7 +65,7 @@ beforeAll(() => { mutationResults = mutationFileNames.map(async fileName => { // Wait for the schema to resolve. We need the schema to be introspected // before we can do anything else! - let { gqlSchema, dSchema, rbacSchema } = await gqlSchemaPromise; + let { gqlSchema, dSchema, pg10Schema, rbacSchema } = await gqlSchemaPromise; // Get a new Postgres client and run the mutation. return await withPgClient(async pgClient => { // Read the mutation from the file system. @@ -71,6 +80,14 @@ beforeAll(() => { let schemaToUse; if (fileName.startsWith("d.")) { schemaToUse = dSchema; + } else if (fileName.startsWith("pg10.")) { + const serverVersionNum = await getServerVersionNum(pgClient); + if (serverVersionNum < 100000) { + // eslint-disable-next-line + console.log("Skipping test as PG version is less than 10"); + return; + } + schemaToUse = pg10Schema; } else if (fileName.startsWith("rbac.")) { await pgClient.query( "select set_config('role', 'postgraphile_test_visitor', true), set_config('jwt.claims.user_id', '3', true)" diff --git a/packages/postgraphile-core/__tests__/integration/schema/__snapshots__/pg10.test.js.snap b/packages/postgraphile-core/__tests__/integration/schema/__snapshots__/pg10.test.js.snap new file mode 100644 index 000000000..b1164aa85 --- /dev/null +++ b/packages/postgraphile-core/__tests__/integration/schema/__snapshots__/pg10.test.js.snap @@ -0,0 +1,645 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`prints a schema to test PG10-specific features 1`] = ` +"\\"\\"\\"A connection to a list of \`AlwaysAsIdentity\` values.\\"\\"\\" +type AlwaysAsIdentitiesConnection { + \\"\\"\\" + A list of edges which contains the \`AlwaysAsIdentity\` and cursor to aid in pagination. + \\"\\"\\" + edges: [AlwaysAsIdentitiesEdge!]! + + \\"\\"\\"A list of \`AlwaysAsIdentity\` objects.\\"\\"\\" + nodes: [AlwaysAsIdentity]! + + \\"\\"\\"Information to aid in pagination.\\"\\"\\" + pageInfo: PageInfo! + + \\"\\"\\" + The count of *all* \`AlwaysAsIdentity\` you could get from the connection. + \\"\\"\\" + totalCount: Int +} + +\\"\\"\\"A \`AlwaysAsIdentity\` edge in the connection.\\"\\"\\" +type AlwaysAsIdentitiesEdge { + \\"\\"\\"A cursor for use in pagination.\\"\\"\\" + cursor: Cursor + + \\"\\"\\"The \`AlwaysAsIdentity\` at the end of the edge.\\"\\"\\" + node: AlwaysAsIdentity +} + +\\"\\"\\"Methods to use when ordering \`AlwaysAsIdentity\`.\\"\\"\\" +enum AlwaysAsIdentitiesOrderBy { + ID_ASC + ID_DESC + NATURAL + PRIMARY_KEY_ASC + PRIMARY_KEY_DESC + T_ASC + T_DESC +} + +type AlwaysAsIdentity implements Node { + id: Int! + + \\"\\"\\" + A globally unique identifier. Can be used in various places throughout the system to identify this single value. + \\"\\"\\" + nodeId: ID! + t: String +} + +\\"\\"\\" +A condition to be used against \`AlwaysAsIdentity\` object types. All fields are +tested for equality and combined with a logical ‘and.’ +\\"\\"\\" +input AlwaysAsIdentityCondition { + \\"\\"\\"Checks for equality with the object’s \`id\` field.\\"\\"\\" + id: Int + + \\"\\"\\"Checks for equality with the object’s \`t\` field.\\"\\"\\" + t: String +} + +\\"\\"\\"An input for mutations affecting \`AlwaysAsIdentity\`\\"\\"\\" +input AlwaysAsIdentityInput { + t: String +} + +\\"\\"\\" +Represents an update to a \`AlwaysAsIdentity\`. Fields that are set will be updated. +\\"\\"\\" +input AlwaysAsIdentityPatch { + t: String +} + +\\"\\"\\"A connection to a list of \`ByDefaultAsIdentity\` values.\\"\\"\\" +type ByDefaultAsIdentitiesConnection { + \\"\\"\\" + A list of edges which contains the \`ByDefaultAsIdentity\` and cursor to aid in pagination. + \\"\\"\\" + edges: [ByDefaultAsIdentitiesEdge!]! + + \\"\\"\\"A list of \`ByDefaultAsIdentity\` objects.\\"\\"\\" + nodes: [ByDefaultAsIdentity]! + + \\"\\"\\"Information to aid in pagination.\\"\\"\\" + pageInfo: PageInfo! + + \\"\\"\\" + The count of *all* \`ByDefaultAsIdentity\` you could get from the connection. + \\"\\"\\" + totalCount: Int +} + +\\"\\"\\"A \`ByDefaultAsIdentity\` edge in the connection.\\"\\"\\" +type ByDefaultAsIdentitiesEdge { + \\"\\"\\"A cursor for use in pagination.\\"\\"\\" + cursor: Cursor + + \\"\\"\\"The \`ByDefaultAsIdentity\` at the end of the edge.\\"\\"\\" + node: ByDefaultAsIdentity +} + +\\"\\"\\"Methods to use when ordering \`ByDefaultAsIdentity\`.\\"\\"\\" +enum ByDefaultAsIdentitiesOrderBy { + ID_ASC + ID_DESC + NATURAL + PRIMARY_KEY_ASC + PRIMARY_KEY_DESC + T_ASC + T_DESC +} + +type ByDefaultAsIdentity implements Node { + id: Int! + + \\"\\"\\" + A globally unique identifier. Can be used in various places throughout the system to identify this single value. + \\"\\"\\" + nodeId: ID! + t: String +} + +\\"\\"\\" +A condition to be used against \`ByDefaultAsIdentity\` object types. All fields +are tested for equality and combined with a logical ‘and.’ +\\"\\"\\" +input ByDefaultAsIdentityCondition { + \\"\\"\\"Checks for equality with the object’s \`id\` field.\\"\\"\\" + id: Int + + \\"\\"\\"Checks for equality with the object’s \`t\` field.\\"\\"\\" + t: String +} + +\\"\\"\\"An input for mutations affecting \`ByDefaultAsIdentity\`\\"\\"\\" +input ByDefaultAsIdentityInput { + id: Int + t: String +} + +\\"\\"\\" +Represents an update to a \`ByDefaultAsIdentity\`. Fields that are set will be updated. +\\"\\"\\" +input ByDefaultAsIdentityPatch { + id: Int + t: String +} + +\\"\\"\\"All input for the create \`AlwaysAsIdentity\` mutation.\\"\\"\\" +input CreateAlwaysAsIdentityInput { + \\"\\"\\"The \`AlwaysAsIdentity\` to be created by this mutation.\\"\\"\\" + alwaysAsIdentity: AlwaysAsIdentityInput! + + \\"\\"\\" + 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 output of our create \`AlwaysAsIdentity\` mutation.\\"\\"\\" +type CreateAlwaysAsIdentityPayload { + \\"\\"\\"The \`AlwaysAsIdentity\` that was created by this mutation.\\"\\"\\" + alwaysAsIdentity: AlwaysAsIdentity + + \\"\\"\\"An edge for our \`AlwaysAsIdentity\`. May be used by Relay 1.\\"\\"\\" + alwaysAsIdentityEdge( + \\"\\"\\"The method to use when ordering \`AlwaysAsIdentity\`.\\"\\"\\" + orderBy: [AlwaysAsIdentitiesOrderBy!] = [PRIMARY_KEY_ASC] + ): AlwaysAsIdentitiesEdge + + \\"\\"\\" + 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 + + \\"\\"\\" + Our root query field type. Allows us to run any query from our mutation payload. + \\"\\"\\" + query: Query +} + +\\"\\"\\"All input for the create \`ByDefaultAsIdentity\` mutation.\\"\\"\\" +input CreateByDefaultAsIdentityInput { + \\"\\"\\"The \`ByDefaultAsIdentity\` to be created by this mutation.\\"\\"\\" + byDefaultAsIdentity: ByDefaultAsIdentityInput! + + \\"\\"\\" + 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 output of our create \`ByDefaultAsIdentity\` mutation.\\"\\"\\" +type CreateByDefaultAsIdentityPayload { + \\"\\"\\"The \`ByDefaultAsIdentity\` that was created by this mutation.\\"\\"\\" + byDefaultAsIdentity: ByDefaultAsIdentity + + \\"\\"\\"An edge for our \`ByDefaultAsIdentity\`. May be used by Relay 1.\\"\\"\\" + byDefaultAsIdentityEdge( + \\"\\"\\"The method to use when ordering \`ByDefaultAsIdentity\`.\\"\\"\\" + orderBy: [ByDefaultAsIdentitiesOrderBy!] = [PRIMARY_KEY_ASC] + ): ByDefaultAsIdentitiesEdge + + \\"\\"\\" + 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 + + \\"\\"\\" + 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 \`deleteAlwaysAsIdentityById\` mutation.\\"\\"\\" +input DeleteAlwaysAsIdentityByIdInput { + \\"\\"\\" + 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 \`deleteAlwaysAsIdentity\` mutation.\\"\\"\\" +input DeleteAlwaysAsIdentityInput { + \\"\\"\\" + 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 \`AlwaysAsIdentity\` to be deleted. + \\"\\"\\" + nodeId: ID! +} + +\\"\\"\\"The output of our delete \`AlwaysAsIdentity\` mutation.\\"\\"\\" +type DeleteAlwaysAsIdentityPayload { + \\"\\"\\"The \`AlwaysAsIdentity\` that was deleted by this mutation.\\"\\"\\" + alwaysAsIdentity: AlwaysAsIdentity + + \\"\\"\\"An edge for our \`AlwaysAsIdentity\`. May be used by Relay 1.\\"\\"\\" + alwaysAsIdentityEdge( + \\"\\"\\"The method to use when ordering \`AlwaysAsIdentity\`.\\"\\"\\" + orderBy: [AlwaysAsIdentitiesOrderBy!] = [PRIMARY_KEY_ASC] + ): AlwaysAsIdentitiesEdge + + \\"\\"\\" + 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 + deletedAlwaysAsIdentityId: ID + + \\"\\"\\" + Our root query field type. Allows us to run any query from our mutation payload. + \\"\\"\\" + query: Query +} + +\\"\\"\\"All input for the \`deleteByDefaultAsIdentityById\` mutation.\\"\\"\\" +input DeleteByDefaultAsIdentityByIdInput { + \\"\\"\\" + 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 \`deleteByDefaultAsIdentity\` mutation.\\"\\"\\" +input DeleteByDefaultAsIdentityInput { + \\"\\"\\" + 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 \`ByDefaultAsIdentity\` to be deleted. + \\"\\"\\" + nodeId: ID! +} + +\\"\\"\\"The output of our delete \`ByDefaultAsIdentity\` mutation.\\"\\"\\" +type DeleteByDefaultAsIdentityPayload { + \\"\\"\\"The \`ByDefaultAsIdentity\` that was deleted by this mutation.\\"\\"\\" + byDefaultAsIdentity: ByDefaultAsIdentity + + \\"\\"\\"An edge for our \`ByDefaultAsIdentity\`. May be used by Relay 1.\\"\\"\\" + byDefaultAsIdentityEdge( + \\"\\"\\"The method to use when ordering \`ByDefaultAsIdentity\`.\\"\\"\\" + orderBy: [ByDefaultAsIdentitiesOrderBy!] = [PRIMARY_KEY_ASC] + ): ByDefaultAsIdentitiesEdge + + \\"\\"\\" + 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 + deletedByDefaultAsIdentityId: ID + + \\"\\"\\" + Our root query field type. Allows us to run any query from our mutation payload. + \\"\\"\\" + query: Query +} + +\\"\\"\\" +The root mutation type which contains root level fields which mutate data. +\\"\\"\\" +type Mutation { + \\"\\"\\"Creates a single \`AlwaysAsIdentity\`.\\"\\"\\" + createAlwaysAsIdentity( + \\"\\"\\" + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + \\"\\"\\" + input: CreateAlwaysAsIdentityInput! + ): CreateAlwaysAsIdentityPayload + + \\"\\"\\"Creates a single \`ByDefaultAsIdentity\`.\\"\\"\\" + createByDefaultAsIdentity( + \\"\\"\\" + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + \\"\\"\\" + input: CreateByDefaultAsIdentityInput! + ): CreateByDefaultAsIdentityPayload + + \\"\\"\\"Deletes a single \`AlwaysAsIdentity\` using its globally unique id.\\"\\"\\" + deleteAlwaysAsIdentity( + \\"\\"\\" + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + \\"\\"\\" + input: DeleteAlwaysAsIdentityInput! + ): DeleteAlwaysAsIdentityPayload + + \\"\\"\\"Deletes a single \`AlwaysAsIdentity\` using a unique key.\\"\\"\\" + deleteAlwaysAsIdentityById( + \\"\\"\\" + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + \\"\\"\\" + input: DeleteAlwaysAsIdentityByIdInput! + ): DeleteAlwaysAsIdentityPayload + + \\"\\"\\"Deletes a single \`ByDefaultAsIdentity\` using its globally unique id.\\"\\"\\" + deleteByDefaultAsIdentity( + \\"\\"\\" + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + \\"\\"\\" + input: DeleteByDefaultAsIdentityInput! + ): DeleteByDefaultAsIdentityPayload + + \\"\\"\\"Deletes a single \`ByDefaultAsIdentity\` using a unique key.\\"\\"\\" + deleteByDefaultAsIdentityById( + \\"\\"\\" + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + \\"\\"\\" + input: DeleteByDefaultAsIdentityByIdInput! + ): DeleteByDefaultAsIdentityPayload + + \\"\\"\\" + Updates a single \`AlwaysAsIdentity\` using its globally unique id and a patch. + \\"\\"\\" + updateAlwaysAsIdentity( + \\"\\"\\" + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + \\"\\"\\" + input: UpdateAlwaysAsIdentityInput! + ): UpdateAlwaysAsIdentityPayload + + \\"\\"\\"Updates a single \`AlwaysAsIdentity\` using a unique key and a patch.\\"\\"\\" + updateAlwaysAsIdentityById( + \\"\\"\\" + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + \\"\\"\\" + input: UpdateAlwaysAsIdentityByIdInput! + ): UpdateAlwaysAsIdentityPayload + + \\"\\"\\" + Updates a single \`ByDefaultAsIdentity\` using its globally unique id and a patch. + \\"\\"\\" + updateByDefaultAsIdentity( + \\"\\"\\" + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + \\"\\"\\" + input: UpdateByDefaultAsIdentityInput! + ): UpdateByDefaultAsIdentityPayload + + \\"\\"\\" + Updates a single \`ByDefaultAsIdentity\` using a unique key and a patch. + \\"\\"\\" + updateByDefaultAsIdentityById( + \\"\\"\\" + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + \\"\\"\\" + input: UpdateByDefaultAsIdentityByIdInput! + ): UpdateByDefaultAsIdentityPayload +} + +\\"\\"\\"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 +} + +\\"\\"\\"The root query type which gives access points into the data universe.\\"\\"\\" +type Query implements Node { + \\"\\"\\"Reads and enables pagination through a set of \`AlwaysAsIdentity\`.\\"\\"\\" + allAlwaysAsIdentities( + \\"\\"\\"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: AlwaysAsIdentityCondition + + \\"\\"\\"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 \`AlwaysAsIdentity\`.\\"\\"\\" + orderBy: [AlwaysAsIdentitiesOrderBy!] = [PRIMARY_KEY_ASC] + ): AlwaysAsIdentitiesConnection + + \\"\\"\\"Reads and enables pagination through a set of \`ByDefaultAsIdentity\`.\\"\\"\\" + allByDefaultAsIdentities( + \\"\\"\\"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: ByDefaultAsIdentityCondition + + \\"\\"\\"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 \`ByDefaultAsIdentity\`.\\"\\"\\" + orderBy: [ByDefaultAsIdentitiesOrderBy!] = [PRIMARY_KEY_ASC] + ): ByDefaultAsIdentitiesConnection + + \\"\\"\\"Reads a single \`AlwaysAsIdentity\` using its globally unique \`ID\`.\\"\\"\\" + alwaysAsIdentity( + \\"\\"\\" + The globally unique \`ID\` to be used in selecting a single \`AlwaysAsIdentity\`. + \\"\\"\\" + nodeId: ID! + ): AlwaysAsIdentity + alwaysAsIdentityById(id: Int!): AlwaysAsIdentity + + \\"\\"\\"Reads a single \`ByDefaultAsIdentity\` using its globally unique \`ID\`.\\"\\"\\" + byDefaultAsIdentity( + \\"\\"\\" + The globally unique \`ID\` to be used in selecting a single \`ByDefaultAsIdentity\`. + \\"\\"\\" + nodeId: ID! + ): ByDefaultAsIdentity + byDefaultAsIdentityById(id: Int!): ByDefaultAsIdentity + + \\"\\"\\"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 \`updateAlwaysAsIdentityById\` mutation.\\"\\"\\" +input UpdateAlwaysAsIdentityByIdInput { + \\"\\"\\" + An object where the defined keys will be set on the \`AlwaysAsIdentity\` being updated. + \\"\\"\\" + alwaysAsIdentityPatch: AlwaysAsIdentityPatch! + + \\"\\"\\" + 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 \`updateAlwaysAsIdentity\` mutation.\\"\\"\\" +input UpdateAlwaysAsIdentityInput { + \\"\\"\\" + An object where the defined keys will be set on the \`AlwaysAsIdentity\` being updated. + \\"\\"\\" + alwaysAsIdentityPatch: AlwaysAsIdentityPatch! + + \\"\\"\\" + 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 \`AlwaysAsIdentity\` to be updated. + \\"\\"\\" + nodeId: ID! +} + +\\"\\"\\"The output of our update \`AlwaysAsIdentity\` mutation.\\"\\"\\" +type UpdateAlwaysAsIdentityPayload { + \\"\\"\\"The \`AlwaysAsIdentity\` that was updated by this mutation.\\"\\"\\" + alwaysAsIdentity: AlwaysAsIdentity + + \\"\\"\\"An edge for our \`AlwaysAsIdentity\`. May be used by Relay 1.\\"\\"\\" + alwaysAsIdentityEdge( + \\"\\"\\"The method to use when ordering \`AlwaysAsIdentity\`.\\"\\"\\" + orderBy: [AlwaysAsIdentitiesOrderBy!] = [PRIMARY_KEY_ASC] + ): AlwaysAsIdentitiesEdge + + \\"\\"\\" + 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 + + \\"\\"\\" + Our root query field type. Allows us to run any query from our mutation payload. + \\"\\"\\" + query: Query +} + +\\"\\"\\"All input for the \`updateByDefaultAsIdentityById\` mutation.\\"\\"\\" +input UpdateByDefaultAsIdentityByIdInput { + \\"\\"\\" + An object where the defined keys will be set on the \`ByDefaultAsIdentity\` being updated. + \\"\\"\\" + byDefaultAsIdentityPatch: ByDefaultAsIdentityPatch! + + \\"\\"\\" + 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 \`updateByDefaultAsIdentity\` mutation.\\"\\"\\" +input UpdateByDefaultAsIdentityInput { + \\"\\"\\" + An object where the defined keys will be set on the \`ByDefaultAsIdentity\` being updated. + \\"\\"\\" + byDefaultAsIdentityPatch: ByDefaultAsIdentityPatch! + + \\"\\"\\" + 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 \`ByDefaultAsIdentity\` to be updated. + \\"\\"\\" + nodeId: ID! +} + +\\"\\"\\"The output of our update \`ByDefaultAsIdentity\` mutation.\\"\\"\\" +type UpdateByDefaultAsIdentityPayload { + \\"\\"\\"The \`ByDefaultAsIdentity\` that was updated by this mutation.\\"\\"\\" + byDefaultAsIdentity: ByDefaultAsIdentity + + \\"\\"\\"An edge for our \`ByDefaultAsIdentity\`. May be used by Relay 1.\\"\\"\\" + byDefaultAsIdentityEdge( + \\"\\"\\"The method to use when ordering \`ByDefaultAsIdentity\`.\\"\\"\\" + orderBy: [ByDefaultAsIdentitiesOrderBy!] = [PRIMARY_KEY_ASC] + ): ByDefaultAsIdentitiesEdge + + \\"\\"\\" + 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 + + \\"\\"\\" + 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/pg10.test.js b/packages/postgraphile-core/__tests__/integration/schema/pg10.test.js new file mode 100644 index 000000000..885b4c70a --- /dev/null +++ b/packages/postgraphile-core/__tests__/integration/schema/pg10.test.js @@ -0,0 +1,3 @@ +const core = require("./core"); + +test("prints a schema to test PG10-specific features", core.test(["pg10"])); diff --git a/packages/postgraphile-core/__tests__/pg10.sql b/packages/postgraphile-core/__tests__/pg10.sql new file mode 100644 index 000000000..970831555 --- /dev/null +++ b/packages/postgraphile-core/__tests__/pg10.sql @@ -0,0 +1,12 @@ +drop schema if exists pg10 cascade; +create schema pg10; + +create table pg10.always_as_identity ( + id int primary key generated always as identity, + t text +); + +create table pg10.by_default_as_identity ( + id int primary key generated by default as identity, + t text +); \ No newline at end of file diff --git a/packages/postgraphile-core/scripts/test b/packages/postgraphile-core/scripts/test index 0be3ca70e..e46352e46 100755 --- a/packages/postgraphile-core/scripts/test +++ b/packages/postgraphile-core/scripts/test @@ -34,8 +34,19 @@ grant postgraphile_test_user2 to postgraphile_test_authenticator; HERE psql -Xqv ON_ERROR_STOP=1 -f __tests__/kitchen-sink-schema.sql "$TEST_DATABASE_URL" psql -Xqv ON_ERROR_STOP=1 -f __tests__/kitchen-sink-permissions.sql "$TEST_DATABASE_URL" + +SERVER_VERSION_NUM=$(psql -Xqtv ON_ERROR_STOP=1 -c "show server_version_num;" "$TEST_DATABASE_URL") + +if [ $SERVER_VERSION_NUM -ge 100000 ]; then + psql -Xqv ON_ERROR_STOP=1 -f __tests__/pg10.sql "$TEST_DATABASE_URL" +fi; + clear echo "Database reset successfully ✅" # Now run the tests -jest -i $@ +if [ $SERVER_VERSION_NUM -ge 100000 ]; then + jest -i $@ +else + jest -i -t "^((?!pg10).)*$" $@ +fi;