From ee58471aed1e0f1b3208b7a6e84993e49233321c Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Sun, 6 Feb 2022 15:23:07 -0500 Subject: [PATCH 1/5] WIP: graphql revisions operations --- src/collections/graphql/init.ts | 1 + .../graphql/resolvers/findVersionByID.ts | 37 +++++++ .../graphql/resolvers/findVersions.ts | 45 ++++++++ src/collections/operations/create.ts | 1 + src/graphql/bindResolvers.ts | 6 ++ src/types/index.ts | 2 +- src/versions/tests/collections-rest.spec.ts | 8 +- src/versions/tests/graphql.spec.ts | 100 ++++++++++++++++++ 8 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 src/collections/graphql/resolvers/findVersionByID.ts create mode 100644 src/collections/graphql/resolvers/findVersions.ts create mode 100644 src/versions/tests/graphql.spec.ts diff --git a/src/collections/graphql/init.ts b/src/collections/graphql/init.ts index 1d454a82d3..31eb1b0094 100644 --- a/src/collections/graphql/init.ts +++ b/src/collections/graphql/init.ts @@ -14,6 +14,7 @@ import { getCollectionIDType } from '../../graphql/schema/buildMutationInputType function registerCollections(): void { const { + // TODO: findVersions, findVersionByID, publishVersion create, find, findByID, deleteResolver, update, } = this.graphQL.resolvers.collections; diff --git a/src/collections/graphql/resolvers/findVersionByID.ts b/src/collections/graphql/resolvers/findVersionByID.ts new file mode 100644 index 0000000000..5c547c9fbf --- /dev/null +++ b/src/collections/graphql/resolvers/findVersionByID.ts @@ -0,0 +1,37 @@ +/* eslint-disable no-param-reassign */ +import { Response } from 'express'; +import { Collection } from '../../config/types'; +import { PayloadRequest } from '../../../express/types'; + +export type Resolver = ( + _: unknown, + args: { + locale?: string + fallbackLocale?: string + }, + context: { + req: PayloadRequest, + res: Response + } +) => Promise + +export default function findVersionByID(collection: Collection): Resolver { + async function resolver(_, args, context) { + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; + + const options = { + collection, + id: args.id, + req: context.req, + draft: args.draft, + }; + + const result = await this.operations.collections.findVersionByID(options); + + return result; + } + + const findVersionByIDResolver = resolver.bind(this); + return findVersionByIDResolver; +} diff --git a/src/collections/graphql/resolvers/findVersions.ts b/src/collections/graphql/resolvers/findVersions.ts new file mode 100644 index 0000000000..741a164cbf --- /dev/null +++ b/src/collections/graphql/resolvers/findVersions.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-param-reassign */ + +import { Response } from 'express'; +import { Where } from '../../../types'; +import { PayloadRequest } from '../../../express/types'; +import { Collection } from '../../config/types'; + +export type Resolver = ( + _: unknown, + args: { + locale?: string + fallbackLocale?: string + where: Where + limit?: number + page?: number + sort?: string + }, + context: { + req: PayloadRequest, + res: Response + } +) => Promise + +export default function findVersions(collection: Collection): Resolver { + async function resolver(_, args, context) { + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; + + const options = { + collection, + where: args.where, + limit: args.limit, + page: args.page, + sort: args.sort, + req: context.req, + }; + + const result = await this.operations.collections.findVersions(options); + + return result; + } + + const findVersionsResolver = resolver.bind(this); + return findVersionsResolver; +} diff --git a/src/collections/operations/create.ts b/src/collections/operations/create.ts index df0dc85ff8..632dda9087 100644 --- a/src/collections/operations/create.ts +++ b/src/collections/operations/create.ts @@ -185,6 +185,7 @@ async function create(this: Payload, incomingArgs: Arguments): Promise } let result: Document = doc.toJSON({ virtuals: true }); + // TODO: default status to 'draft'; const verificationToken = result._verificationToken; // custom id type reset diff --git a/src/graphql/bindResolvers.ts b/src/graphql/bindResolvers.ts index a61b2d1775..786f05f2fa 100644 --- a/src/graphql/bindResolvers.ts +++ b/src/graphql/bindResolvers.ts @@ -14,6 +14,8 @@ import find from '../collections/graphql/resolvers/find'; import findByID from '../collections/graphql/resolvers/findByID'; import update from '../collections/graphql/resolvers/update'; import deleteResolver from '../collections/graphql/resolvers/delete'; +import findVersions from '../collections/graphql/resolvers/findVersions'; +import findVersionByID from '../collections/graphql/resolvers/findVersionByID'; import findOne from '../globals/graphql/resolvers/findOne'; import globalUpdate from '../globals/graphql/resolvers/update'; @@ -24,7 +26,9 @@ export type GraphQLResolvers = { collections: { create: typeof create, find: typeof find, + findVersions: typeof findVersions, findByID: typeof findByID, + findVersionByID: typeof findVersionByID, update: typeof update, deleteResolver: typeof deleteResolver, auth: { @@ -52,7 +56,9 @@ function bindResolvers(ctx: Payload): void { collections: { create: create.bind(ctx), find: find.bind(ctx), + findVersions: findVersions.bind(ctx), findByID: findByID.bind(ctx), + findVersionByID: findVersionByID.bind(ctx), update: update.bind(ctx), deleteResolver: deleteResolver.bind(ctx), auth: { diff --git a/src/types/index.ts b/src/types/index.ts index 19619308b6..a399b6adf2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,5 @@ import { Document as MongooseDocument } from 'mongoose'; -import { TypeWithID, TypeWithTimestamps } from '../collections/config/types'; +import { TypeWithTimestamps } from '../collections/config/types'; import { FileData } from '../uploads/types'; export type Operator = 'equals' diff --git a/src/versions/tests/collections-rest.spec.ts b/src/versions/tests/collections-rest.spec.ts index 9f529977d0..54be35869e 100644 --- a/src/versions/tests/collections-rest.spec.ts +++ b/src/versions/tests/collections-rest.spec.ts @@ -58,14 +58,16 @@ describe('Collection Versions - REST', () => { method: 'put', }).then((res) => res.json()); - expect(updatedPost.doc.title).toBe(title2); - expect(updatedPost.doc._status).toStrictEqual('draft'); - const versions = await fetch(`${url}/api/autosave-posts/versions`, { headers, }).then((res) => res.json()); versionID = versions.docs[0].id; + + expect(updatedPost.doc.title).toBe(title2); + expect(updatedPost.doc._status).toStrictEqual('draft'); + + expect(versionID).toBeDefined(); }); it('should allow a version to be retrieved by ID', async () => { diff --git a/src/versions/tests/graphql.spec.ts b/src/versions/tests/graphql.spec.ts new file mode 100644 index 0000000000..03b060fe0e --- /dev/null +++ b/src/versions/tests/graphql.spec.ts @@ -0,0 +1,100 @@ +/** + * @jest-environment node + */ +import { request, GraphQLClient } from 'graphql-request'; +import getConfig from '../../config/load'; +import { email, password } from '../../mongoose/testCredentials'; + +require('isomorphic-fetch'); + +const config = getConfig(); + +const url = `${config.serverURL}${config.routes.api}${config.routes.graphQL}`; + +let client; +let token; +let postID; +let versionID; + +describe('GrahpQL Resolvers', () => { + beforeAll(async (done) => { + const login = ` + mutation { + loginAdmin( + email: "${email}", + password: "${password}" + ) { + token + } + }`; + + const response = await request(url, login); + + token = response.loginAdmin.token; + + client = new GraphQLClient(url, { headers: { Authorization: `JWT ${token}` } }); + + done(); + }); + + describe('Create', () => { + it('should allow a new autosavePost to be created with draft status', async () => { + const title = 'autosave title'; + const description = 'autosave description'; + + const query = `mutation { + createAutosavePost(data: {title: "${title}", description: "${description}"}) { + id + title + description + createdAt + updatedAt + } + }`; + + const response = await client.request(query); + + const data = response.createAutosavePost; + postID = data.id; + + expect(typeof data._status).toBe('undefined'); + }); + }); + + describe('Read', () => { + it('should allow read of autosavePost versions', async () => { + // language=graphQL + const query = `query { + versionsAutosavePost(where: { parent: { equals: ${postID} } }) { + id + version { + title + } + } + }`; + + const response = await client.request(query); + + const data = response.versionsAutosavePost; + + versionID = data.docs[0].id; + + expect(versionID).toBeDefined(); + expect(data.docs[0].version.title).toBeDefined(); + }); + }); + + // describe('Restore', () => { + // it('should allow a version to be restored', async () => { + // // update a versionsPost + // const query = `mutation { + // restoreAutosavePost { + // id + // title + // }`; + // const response = await client.request(query); + // + // const data = response.versionsAutosavePost; + // }); + // }); +}); From 7cfb2f7f02a46b3883dc0f6cb914d3ef09c16ffe Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Thu, 10 Feb 2022 15:23:39 -0500 Subject: [PATCH 2/5] feat: GraphQL version collection resolvers --- src/collections/graphql/init.ts | 32 +- .../graphql/resolvers/publishVersion.ts | 35 ++ src/collections/operations/create.ts | 1 - src/graphql/bindResolvers.ts | 3 + src/graphql/index.ts | 6 +- src/graphql/schema/buildInputObject.ts | 34 ++ src/graphql/schema/buildVersionType.ts | 19 + .../schema/buildVersionWhereInputType.ts | 47 +++ src/graphql/schema/buildWhereInputType.ts | 333 +----------------- src/graphql/schema/fieldToSchemaMap.ts | 256 ++++++++++++++ src/graphql/schema/operators.ts | 6 + .../schema/recursivelyBuildNestedPaths.ts | 42 +++ src/versions/tests/graphql.spec.ts | 76 ++-- 13 files changed, 535 insertions(+), 355 deletions(-) create mode 100644 src/collections/graphql/resolvers/publishVersion.ts create mode 100644 src/graphql/schema/buildInputObject.ts create mode 100644 src/graphql/schema/buildVersionType.ts create mode 100644 src/graphql/schema/buildVersionWhereInputType.ts create mode 100644 src/graphql/schema/fieldToSchemaMap.ts create mode 100644 src/graphql/schema/operators.ts create mode 100644 src/graphql/schema/recursivelyBuildNestedPaths.ts diff --git a/src/collections/graphql/init.ts b/src/collections/graphql/init.ts index 31eb1b0094..a15a209684 100644 --- a/src/collections/graphql/init.ts +++ b/src/collections/graphql/init.ts @@ -11,10 +11,11 @@ import formatName from '../../graphql/utilities/formatName'; import buildPaginatedListType from '../../graphql/schema/buildPaginatedListType'; import { BaseFields } from './types'; import { getCollectionIDType } from '../../graphql/schema/buildMutationInputType'; +import buildVersionWhereInputType from '../../graphql/schema/buildVersionWhereInputType'; function registerCollections(): void { const { - // TODO: findVersions, findVersionByID, publishVersion + findVersions, findVersionByID, publishVersion, create, find, findByID, deleteResolver, update, } = this.graphQL.resolvers.collections; @@ -180,6 +181,35 @@ function registerCollections(): void { resolve: deleteResolver(collection), }; + if (collection.config.versions) { + collection.graphQL.versionType = this.buildVersionType(collection.graphQL.type); + this.Query.fields[`version${formatName(singularLabel)}`] = { + type: collection.graphQL.versionType, + args: { + id: { type: GraphQLString }, + }, + resolve: findVersionByID(collection), + }; + this.Query.fields[`versions${pluralLabel}`] = { + type: buildPaginatedListType(`versions${formatName(pluralLabel)}`, collection.graphQL.versionType), + args: { + where: { type: buildVersionWhereInputType(singularLabel, collection.config) }, + autosave: { type: GraphQLBoolean }, + page: { type: GraphQLInt }, + limit: { type: GraphQLInt }, + sort: { type: GraphQLString }, + }, + resolve: findVersions(collection), + }; + this.Mutation.fields[`restoreVersion${formatName(singularLabel)}`] = { + type: new GraphQLNonNull(GraphQLBoolean), + args: { + id: { type: GraphQLString }, + }, + resolve: publishVersion(collection), + }; + } + if (collection.config.auth) { collection.graphQL.JWT = this.buildObjectType( formatName(`${slug}JWT`), diff --git a/src/collections/graphql/resolvers/publishVersion.ts b/src/collections/graphql/resolvers/publishVersion.ts new file mode 100644 index 0000000000..6e1a3cbbf4 --- /dev/null +++ b/src/collections/graphql/resolvers/publishVersion.ts @@ -0,0 +1,35 @@ +/* eslint-disable no-param-reassign */ +import { Response } from 'express'; +import { Collection } from '../../config/types'; +import { PayloadRequest } from '../../../express/types'; + +export type Resolver = ( + _: unknown, + args: { + locale?: string + fallbackLocale?: string + }, + context: { + req: PayloadRequest, + res: Response + } +) => Promise + +export default function publishVersion(collection: Collection): Resolver { + async function resolver(_, args, context) { + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; + + const options = { + collection, + id: args.id, + req: context.req, + }; + + await this.operations.collections.publishVersion(options); + return true; + } + + const findVersionByIDResolver = resolver.bind(this); + return findVersionByIDResolver; +} diff --git a/src/collections/operations/create.ts b/src/collections/operations/create.ts index 632dda9087..df0dc85ff8 100644 --- a/src/collections/operations/create.ts +++ b/src/collections/operations/create.ts @@ -185,7 +185,6 @@ async function create(this: Payload, incomingArgs: Arguments): Promise } let result: Document = doc.toJSON({ virtuals: true }); - // TODO: default status to 'draft'; const verificationToken = result._verificationToken; // custom id type reset diff --git a/src/graphql/bindResolvers.ts b/src/graphql/bindResolvers.ts index 786f05f2fa..3d3920ce96 100644 --- a/src/graphql/bindResolvers.ts +++ b/src/graphql/bindResolvers.ts @@ -16,6 +16,7 @@ import update from '../collections/graphql/resolvers/update'; import deleteResolver from '../collections/graphql/resolvers/delete'; import findVersions from '../collections/graphql/resolvers/findVersions'; import findVersionByID from '../collections/graphql/resolvers/findVersionByID'; +import publishVersion from '../collections/graphql/resolvers/publishVersion'; import findOne from '../globals/graphql/resolvers/findOne'; import globalUpdate from '../globals/graphql/resolvers/update'; @@ -29,6 +30,7 @@ export type GraphQLResolvers = { findVersions: typeof findVersions, findByID: typeof findByID, findVersionByID: typeof findVersionByID, + publishVersion: typeof publishVersion, update: typeof update, deleteResolver: typeof deleteResolver, auth: { @@ -59,6 +61,7 @@ function bindResolvers(ctx: Payload): void { findVersions: findVersions.bind(ctx), findByID: findByID.bind(ctx), findVersionByID: findVersionByID.bind(ctx), + publishVersion: publishVersion.bind(ctx), update: update.bind(ctx), deleteResolver: deleteResolver.bind(ctx), auth: { diff --git a/src/graphql/index.ts b/src/graphql/index.ts index 0e5b81bff2..a4ae2b029f 100644 --- a/src/graphql/index.ts +++ b/src/graphql/index.ts @@ -13,7 +13,8 @@ import initCollections from '../collections/graphql/init'; import initGlobals from '../globals/graphql/init'; import initPreferences from '../preferences/graphql/init'; import { GraphQLResolvers } from './bindResolvers'; -import buildWhereInputType from './schema/buildWhereInputType'; +import buildVersionType from './schema/buildVersionType'; +import { buildWhereInputType } from './schema/buildWhereInputType'; import { SanitizedConfig } from '../config/types'; type GraphQLTypes = { @@ -46,6 +47,8 @@ class InitializeGraphQL { buildPoliciesType: typeof buildPoliciesType; + buildVersionType: typeof buildVersionType; + initCollections: typeof initCollections; initGlobals: typeof initGlobals; @@ -90,6 +93,7 @@ class InitializeGraphQL { this.buildWhereInputType = buildWhereInputType; this.buildObjectType = buildObjectType.bind(this); this.buildPoliciesType = buildPoliciesType.bind(this); + this.buildVersionType = buildVersionType.bind(this); this.initCollections = initCollections.bind(this); this.initGlobals = initGlobals.bind(this); this.initPreferences = initPreferences.bind(this); diff --git a/src/graphql/schema/buildInputObject.ts b/src/graphql/schema/buildInputObject.ts new file mode 100644 index 0000000000..d415cd0ee7 --- /dev/null +++ b/src/graphql/schema/buildInputObject.ts @@ -0,0 +1,34 @@ +import { + GraphQLInputFieldConfigMap, + GraphQLInputObjectType, + GraphQLInt, + GraphQLList, + GraphQLString, + Thunk, +} from 'graphql'; + +const buildInputObject = (name: string, fieldTypes: Thunk): GraphQLInputObjectType => { + return new GraphQLInputObjectType({ + name: `${name}_where`, + fields: { + ...fieldTypes, + OR: { + type: new GraphQLList(new GraphQLInputObjectType({ + name: `${name}_where_or`, + fields: fieldTypes, + })), + }, + AND: { + type: new GraphQLList(new GraphQLInputObjectType({ + name: `${name}_where_and`, + fields: fieldTypes, + })), + }, + page: { type: GraphQLInt }, + limit: { type: GraphQLInt }, + sort: { type: GraphQLString }, + }, + }); +}; + +export default buildInputObject; diff --git a/src/graphql/schema/buildVersionType.ts b/src/graphql/schema/buildVersionType.ts new file mode 100644 index 0000000000..f1cfd418c6 --- /dev/null +++ b/src/graphql/schema/buildVersionType.ts @@ -0,0 +1,19 @@ +import { GraphQLBoolean, GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql'; +import { DateTimeResolver } from 'graphql-scalars'; +import formatName from '../utilities/formatName'; + +const buildVersionType = (type: GraphQLObjectType): GraphQLObjectType => { + return new GraphQLObjectType({ + name: formatName(`${type.name}Version`), + fields: { + id: { type: GraphQLString }, + parent: { type: GraphQLString }, + autosave: { type: GraphQLBoolean }, + version: { type }, + updatedAt: { type: new GraphQLNonNull(DateTimeResolver) }, + createdAt: { type: new GraphQLNonNull(DateTimeResolver) }, + }, + }); +}; + +export default buildVersionType; diff --git a/src/graphql/schema/buildVersionWhereInputType.ts b/src/graphql/schema/buildVersionWhereInputType.ts new file mode 100644 index 0000000000..2cbb4f471c --- /dev/null +++ b/src/graphql/schema/buildVersionWhereInputType.ts @@ -0,0 +1,47 @@ +import { + GraphQLBoolean, + GraphQLInputObjectType, + GraphQLString, +} from 'graphql'; +import { DateTimeResolver } from 'graphql-scalars'; +import { GraphQLJSON } from 'graphql-type-json'; +import formatName from '../utilities/formatName'; +import withOperators from './withOperators'; +import { FieldAffectingData } from '../../fields/config/types'; +import buildInputObject from './buildInputObject'; +import { operators } from './operators'; +import { SanitizedCollectionConfig } from '../../collections/config/types'; +import { recursivelyBuildNestedPaths } from './recursivelyBuildNestedPaths'; + +const buildVersionWhereInputType = (singularLabel: string, parentCollection: SanitizedCollectionConfig): GraphQLInputObjectType => { + const name = `version${formatName(singularLabel)}`; + const fieldTypes = { + id: { type: GraphQLString }, + autosave: { type: GraphQLBoolean }, + // TODO: test with custom id field types, may need to support number + updatedAt: { type: DateTimeResolver }, + createdAt: { type: DateTimeResolver }, + parent: { + type: withOperators( + { name: 'parent' } as FieldAffectingData, + GraphQLJSON, + name, + [...operators.equality, ...operators.contains], + ), + }, + }; + + const versionFields = recursivelyBuildNestedPaths(name, { + name: 'version', + type: 'group', + fields: parentCollection.fields, + }); + + versionFields.forEach((versionField) => { + fieldTypes[versionField.key] = versionField.type; + }); + + return buildInputObject(name, fieldTypes); +}; + +export default buildVersionWhereInputType; diff --git a/src/graphql/schema/buildWhereInputType.ts b/src/graphql/schema/buildWhereInputType.ts index a1b3900776..bf9c33fda7 100644 --- a/src/graphql/schema/buildWhereInputType.ts +++ b/src/graphql/schema/buildWhereInputType.ts @@ -1,46 +1,22 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ /* eslint-disable no-use-before-define */ import { - GraphQLBoolean, - GraphQLEnumType, - GraphQLFloat, GraphQLInputObjectType, - GraphQLInt, - GraphQLList, - GraphQLString, } from 'graphql'; import { GraphQLJSON } from 'graphql-type-json'; -import { DateTimeResolver, EmailAddressResolver } from 'graphql-scalars'; import { - optionIsObject, - ArrayField, - CheckboxField, - CodeField, - DateField, - EmailField, Field, - FieldWithSubFields, - GroupField, - NumberField, - RadioField, - RelationshipField, - RichTextField, - RowField, - SelectField, - TextareaField, - TextField, - UploadField, - PointField, FieldAffectingData, - fieldAffectsData, fieldHasSubFields, fieldIsPresentationalOnly, } from '../../fields/config/types'; import formatName from '../utilities/formatName'; -import combineParentName from '../utilities/combineParentName'; import withOperators from './withOperators'; +import { operators } from './operators'; +import buildInputObject from './buildInputObject'; +import { fieldToSchemaMap } from './fieldToSchemaMap'; // buildWhereInputType is similar to buildObjectType and operates // on a field basis with a few distinct differences. @@ -49,284 +25,13 @@ import withOperators from './withOperators'; // 2. Relationships, groups, repeaters and flex content are not // directly searchable. Instead, we need to build a chained pathname // using dot notation so Mongo can properly search nested paths. -const buildWhereInputType = (name: string, fields: Field[], parentName: string): GraphQLInputObjectType => { +export const buildWhereInputType = (name: string, fields: Field[], parentName: string): GraphQLInputObjectType => { // This is the function that builds nested paths for all // field types with nested paths. - const recursivelyBuildNestedPaths = (field: FieldWithSubFields & FieldAffectingData) => { - const nestedPaths = field.fields.reduce((nestedFields, nestedField) => { - if (!fieldIsPresentationalOnly(nestedField)) { - const getFieldSchema = fieldToSchemaMap[nestedField.type]; - const nestedFieldName = fieldAffectsData(nestedField) ? `${field.name}__${nestedField.name}` : undefined; - - if (getFieldSchema) { - const fieldSchema = getFieldSchema({ - ...nestedField, - name: nestedFieldName, - }); - - if (Array.isArray(fieldSchema)) { - return [ - ...nestedFields, - ...fieldSchema, - ]; - } - - return [ - ...nestedFields, - { - key: nestedFieldName, - type: fieldSchema, - }, - ]; - } - } - - return nestedFields; - }, []); - - return nestedPaths; - }; - - const operators = { - equality: ['equals', 'not_equals'], - contains: ['in', 'not_in', 'all'], - comparison: ['greater_than_equal', 'greater_than', 'less_than_equal', 'less_than'], - geo: ['near'], - }; - - const fieldToSchemaMap = { - number: (field: NumberField) => { - const type = GraphQLFloat; - return { - type: withOperators( - field, - type, - parentName, - [...operators.equality, ...operators.comparison], - ), - }; - }, - text: (field: TextField) => { - const type = GraphQLString; - return { - type: withOperators( - field, - type, - parentName, - [...operators.equality, 'like'], - ), - }; - }, - email: (field: EmailField) => { - const type = EmailAddressResolver; - return { - type: withOperators( - field, - type, - parentName, - [...operators.equality, 'like'], - ), - }; - }, - textarea: (field: TextareaField) => { - const type = GraphQLString; - return { - type: withOperators( - field, - type, - parentName, - [...operators.equality, 'like'], - ), - }; - }, - richText: (field: RichTextField) => { - const type = GraphQLJSON; - return { - type: withOperators( - field, - type, - parentName, - [...operators.equality, 'like'], - ), - }; - }, - code: (field: CodeField) => { - const type = GraphQLString; - return { - type: withOperators( - field, - type, - parentName, - [...operators.equality, 'like'], - ), - }; - }, - radio: (field: RadioField) => ({ - type: withOperators( - field, - new GraphQLEnumType({ - name: `${combineParentName(parentName, field.name)}_Input`, - values: field.options.reduce((values, option) => { - if (optionIsObject(option)) { - return { - ...values, - [formatName(option.value)]: { - value: option.value, - }, - }; - } - - return { - ...values, - [formatName(option)]: { - value: option, - }, - }; - }, {}), - }), - parentName, - [...operators.equality, 'like'], - ), - }), - date: (field: DateField) => { - const type = DateTimeResolver; - return { - type: withOperators( - field, - type, - parentName, - [...operators.equality, ...operators.comparison, 'like'], - ), - }; - }, - point: (field: PointField) => { - const type = GraphQLList(GraphQLFloat); - return { - type: withOperators( - field, - type, - parentName, - [...operators.equality, ...operators.comparison, ...operators.geo], - ), - }; - }, - relationship: (field: RelationshipField) => { - let type = withOperators( - field, - GraphQLString, - parentName, - [...operators.equality, ...operators.contains], - ); - - if (Array.isArray(field.relationTo)) { - type = new GraphQLInputObjectType({ - name: `${combineParentName(parentName, field.name)}_Relation`, - fields: { - relationTo: { - type: new GraphQLEnumType({ - name: `${combineParentName(parentName, field.name)}_Relation_RelationTo`, - values: field.relationTo.reduce((values, relation) => ({ - ...values, - [formatName(relation)]: { - value: relation, - }, - }), {}), - }), - }, - value: { type: GraphQLString }, - }, - }); - } - - if (field.hasMany) { - return { - type: new GraphQLList(type), - }; - } - - return { type }; - }, - upload: (field: UploadField) => ({ - type: withOperators( - field, - GraphQLString, - parentName, - [...operators.equality], - ), - }), - checkbox: (field: CheckboxField) => ({ - type: withOperators( - field, - GraphQLBoolean, - parentName, - [...operators.equality], - ), - }), - select: (field: SelectField) => ({ - type: withOperators( - field, - new GraphQLEnumType({ - name: `${combineParentName(parentName, field.name)}_Input`, - values: field.options.reduce((values, option) => { - if (typeof option === 'object' && option.value) { - return { - ...values, - [formatName(option.value)]: { - value: option.value, - }, - }; - } - - if (typeof option === 'string') { - return { - ...values, - [option]: { - value: option, - }, - }; - } - - return values; - }, {}), - }), - parentName, - [...operators.equality, ...operators.contains], - ), - }), - array: (field: ArrayField) => recursivelyBuildNestedPaths(field), - group: (field: GroupField) => recursivelyBuildNestedPaths(field), - row: (field: RowField) => field.fields.reduce((rowSchema, rowField) => { - const getFieldSchema = fieldToSchemaMap[rowField.type]; - - if (getFieldSchema) { - const rowFieldSchema = getFieldSchema(rowField); - - if (fieldHasSubFields(rowField)) { - return [ - ...rowSchema, - ...rowFieldSchema, - ]; - } - - if (fieldAffectsData(rowField)) { - return [ - ...rowSchema, - { - key: rowField.name, - type: rowFieldSchema, - }, - ]; - } - } - - - return rowSchema; - }, []), - }; const fieldTypes = fields.reduce((schema, field) => { if (!fieldIsPresentationalOnly(field) && !field.hidden) { - const getFieldSchema = fieldToSchemaMap[field.type]; + const getFieldSchema = fieldToSchemaMap(parentName)[field.type]; if (getFieldSchema) { const fieldSchema = getFieldSchema(field); @@ -362,31 +67,5 @@ const buildWhereInputType = (name: string, fields: Field[], parentName: string): const fieldName = formatName(name); - return new GraphQLInputObjectType({ - name: `${fieldName}_where`, - fields: { - ...fieldTypes, - OR: { - type: new GraphQLList(new GraphQLInputObjectType({ - name: `${fieldName}_where_or`, - fields: { - ...fieldTypes, - }, - })), - }, - AND: { - type: new GraphQLList(new GraphQLInputObjectType({ - name: `${fieldName}_where_and`, - fields: { - ...fieldTypes, - }, - })), - }, - page: { type: GraphQLInt }, - limit: { type: GraphQLInt }, - sort: { type: GraphQLString }, - }, - }); + return buildInputObject(fieldName, fieldTypes); }; - -export default buildWhereInputType; diff --git a/src/graphql/schema/fieldToSchemaMap.ts b/src/graphql/schema/fieldToSchemaMap.ts new file mode 100644 index 0000000000..39f330751d --- /dev/null +++ b/src/graphql/schema/fieldToSchemaMap.ts @@ -0,0 +1,256 @@ +import { + GraphQLBoolean, + GraphQLEnumType, + GraphQLFloat, + GraphQLInputObjectType, + GraphQLList, + GraphQLString, +} from 'graphql'; +import { DateTimeResolver, EmailAddressResolver } from 'graphql-scalars'; +import { GraphQLJSON } from 'graphql-type-json'; +import { + ArrayField, + CheckboxField, + CodeField, DateField, + EmailField, fieldAffectsData, fieldHasSubFields, GroupField, + NumberField, optionIsObject, PointField, + RadioField, RelationshipField, + RichTextField, RowField, SelectField, + TextareaField, + TextField, UploadField, +} from '../../fields/config/types'; +import withOperators from './withOperators'; +import { operators } from './operators'; +import combineParentName from '../utilities/combineParentName'; +import formatName from '../utilities/formatName'; +import { recursivelyBuildNestedPaths } from './recursivelyBuildNestedPaths'; + +export const fieldToSchemaMap = (parentName: string) => ({ + number: (field: NumberField) => { + const type = GraphQLFloat; + return { + type: withOperators( + field, + type, + parentName, + [...operators.equality, ...operators.comparison], + ), + }; + }, + text: (field: TextField) => { + const type = GraphQLString; + return { + type: withOperators( + field, + type, + parentName, + [...operators.equality, 'like'], + ), + }; + }, + email: (field: EmailField) => { + const type = EmailAddressResolver; + return { + type: withOperators( + field, + type, + parentName, + [...operators.equality, 'like'], + ), + }; + }, + textarea: (field: TextareaField) => { + const type = GraphQLString; + return { + type: withOperators( + field, + type, + parentName, + [...operators.equality, 'like'], + ), + }; + }, + richText: (field: RichTextField) => { + const type = GraphQLJSON; + return { + type: withOperators( + field, + type, + parentName, + [...operators.equality, 'like'], + ), + }; + }, + code: (field: CodeField) => { + const type = GraphQLString; + return { + type: withOperators( + field, + type, + parentName, + [...operators.equality, 'like'], + ), + }; + }, + radio: (field: RadioField) => ({ + type: withOperators( + field, + new GraphQLEnumType({ + name: `${combineParentName(parentName, field.name)}_Input`, + values: field.options.reduce((values, option) => { + if (optionIsObject(option)) { + return { + ...values, + [formatName(option.value)]: { + value: option.value, + }, + }; + } + + return { + ...values, + [formatName(option)]: { + value: option, + }, + }; + }, {}), + }), + parentName, + [...operators.equality, 'like'], + ), + }), + date: (field: DateField) => { + const type = DateTimeResolver; + return { + type: withOperators( + field, + type, + parentName, + [...operators.equality, ...operators.comparison, 'like'], + ), + }; + }, + point: (field: PointField) => { + const type = GraphQLList(GraphQLFloat); + return { + type: withOperators( + field, + type, + parentName, + [...operators.equality, ...operators.comparison, ...operators.geo], + ), + }; + }, + relationship: (field: RelationshipField) => { + let type = withOperators( + field, + GraphQLString, + parentName, + [...operators.equality, ...operators.contains], + ); + + if (Array.isArray(field.relationTo)) { + type = new GraphQLInputObjectType({ + name: `${combineParentName(parentName, field.name)}_Relation`, + fields: { + relationTo: { + type: new GraphQLEnumType({ + name: `${combineParentName(parentName, field.name)}_Relation_RelationTo`, + values: field.relationTo.reduce((values, relation) => ({ + ...values, + [formatName(relation)]: { + value: relation, + }, + }), {}), + }), + }, + value: { type: GraphQLString }, + }, + }); + } + + if (field.hasMany) { + return { + type: new GraphQLList(type), + }; + } + + return { type }; + }, + upload: (field: UploadField) => ({ + type: withOperators( + field, + GraphQLString, + parentName, + [...operators.equality], + ), + }), + checkbox: (field: CheckboxField) => ({ + type: withOperators( + field, + GraphQLBoolean, + parentName, + [...operators.equality], + ), + }), + select: (field: SelectField) => ({ + type: withOperators( + field, + new GraphQLEnumType({ + name: `${combineParentName(parentName, field.name)}_Input`, + values: field.options.reduce((values, option) => { + if (typeof option === 'object' && option.value) { + return { + ...values, + [formatName(option.value)]: { + value: option.value, + }, + }; + } + + if (typeof option === 'string') { + return { + ...values, + [option]: { + value: option, + }, + }; + } + + return values; + }, {}), + }), + parentName, + [...operators.equality, ...operators.contains], + ), + }), + array: (field: ArrayField) => recursivelyBuildNestedPaths(parentName, field), + group: (field: GroupField) => recursivelyBuildNestedPaths(parentName, field), + row: (field: RowField) => field.fields.reduce((rowSchema, rowField) => { + const getFieldSchema = fieldToSchemaMap(parentName)[rowField.type]; + + if (getFieldSchema) { + const rowFieldSchema = getFieldSchema(rowField); + + if (fieldHasSubFields(rowField)) { + return [ + ...rowSchema, + ...rowFieldSchema, + ]; + } + + if (fieldAffectsData(rowField)) { + return [ + ...rowSchema, + { + key: rowField.name, + type: rowFieldSchema, + }, + ]; + } + } + + + return rowSchema; + }, []), +}); diff --git a/src/graphql/schema/operators.ts b/src/graphql/schema/operators.ts new file mode 100644 index 0000000000..b13764d830 --- /dev/null +++ b/src/graphql/schema/operators.ts @@ -0,0 +1,6 @@ +export const operators = { + equality: ['equals', 'not_equals'], + contains: ['in', 'not_in', 'all'], + comparison: ['greater_than_equal', 'greater_than', 'less_than_equal', 'less_than'], + geo: ['near'], +}; diff --git a/src/graphql/schema/recursivelyBuildNestedPaths.ts b/src/graphql/schema/recursivelyBuildNestedPaths.ts new file mode 100644 index 0000000000..572899111a --- /dev/null +++ b/src/graphql/schema/recursivelyBuildNestedPaths.ts @@ -0,0 +1,42 @@ +import { + FieldAffectingData, + fieldAffectsData, + fieldIsPresentationalOnly, + FieldWithSubFields, +} from '../../fields/config/types'; +import { fieldToSchemaMap } from './fieldToSchemaMap'; + +export const recursivelyBuildNestedPaths = (parentName: string, field: FieldWithSubFields & FieldAffectingData) => { + const nestedPaths = field.fields.reduce((nestedFields, nestedField) => { + if (!fieldIsPresentationalOnly(nestedField)) { + const getFieldSchema = fieldToSchemaMap(parentName)[nestedField.type]; + const nestedFieldName = fieldAffectsData(nestedField) ? `${field.name}__${nestedField.name}` : undefined; + + if (getFieldSchema) { + const fieldSchema = getFieldSchema({ + ...nestedField, + name: nestedFieldName, + }); + + if (Array.isArray(fieldSchema)) { + return [ + ...nestedFields, + ...fieldSchema, + ]; + } + + return [ + ...nestedFields, + { + key: nestedFieldName, + type: fieldSchema, + }, + ]; + } + } + + return nestedFields; + }, []); + + return nestedPaths; +}; diff --git a/src/versions/tests/graphql.spec.ts b/src/versions/tests/graphql.spec.ts index 03b060fe0e..350e2ed79a 100644 --- a/src/versions/tests/graphql.spec.ts +++ b/src/versions/tests/graphql.spec.ts @@ -16,7 +16,9 @@ let token; let postID; let versionID; -describe('GrahpQL Resolvers', () => { +describe('GrahpQL Version Resolvers', () => { + const title = 'autosave title'; + beforeAll(async (done) => { const login = ` mutation { @@ -39,7 +41,6 @@ describe('GrahpQL Resolvers', () => { describe('Create', () => { it('should allow a new autosavePost to be created with draft status', async () => { - const title = 'autosave title'; const description = 'autosave description'; const query = `mutation { @@ -49,6 +50,7 @@ describe('GrahpQL Resolvers', () => { description createdAt updatedAt + _status } }`; @@ -57,44 +59,68 @@ describe('GrahpQL Resolvers', () => { const data = response.createAutosavePost; postID = data.id; - expect(typeof data._status).toBe('undefined'); + expect(data._status).toStrictEqual('draft'); }); }); describe('Read', () => { it('should allow read of autosavePost versions', async () => { + const updatedTitle = 'updated title'; + + // modify the post so it will create a new version + // language=graphQL + const update = `mutation { + updateAutosavePost(id: "${postID}", data: {title: "${updatedTitle}"}) { + title + } + }`; + + await client.request(update); + + // query the version // language=graphQL const query = `query { - versionsAutosavePost(where: { parent: { equals: ${postID} } }) { - id - version { - title + versionsAutosavePosts(where: { parent: { equals: "${postID}" } }) { + docs { + id + parent + version { + title + } } } }`; const response = await client.request(query); - const data = response.versionsAutosavePost; + const data = response.versionsAutosavePosts; + const doc = data.docs[0]; + versionID = doc.id; - versionID = data.docs[0].id; - - expect(versionID).toBeDefined(); - expect(data.docs[0].version.title).toBeDefined(); + expect(doc.id).toBeDefined(); + expect(doc.parent).toStrictEqual(postID); + expect(doc.version.title).toStrictEqual(title); }); }); - // describe('Restore', () => { - // it('should allow a version to be restored', async () => { - // // update a versionsPost - // const query = `mutation { - // restoreAutosavePost { - // id - // title - // }`; - // const response = await client.request(query); - // - // const data = response.versionsAutosavePost; - // }); - // }); + describe('Restore', () => { + it('should allow a version to be restored', async () => { + // update a versionsPost + const restore = `mutation { + restoreVersionAutosavePost(id: "${versionID}") + }`; + + await client.request(restore); + + const query = `query { + AutosavePost(id: "${postID}") { + title + } + }`; + + const response = await client.request(query); + const data = response.AutosavePost; + expect(data.title).toStrictEqual(title); + }); + }); }); From b159e148dbe5e3002952f4a1b33cecbb2031ce04 Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Thu, 10 Feb 2022 16:35:03 -0500 Subject: [PATCH 3/5] chore: rename publishVersion to restoreVersion (#438) --- docs/versions/drafts.mdx | 2 +- docs/versions/overview.mdx | 2 +- src/collections/graphql/init.ts | 4 ++-- .../{publishVersion.ts => restoreVersion.ts} | 8 ++++---- src/collections/init.ts | 4 ++-- src/collections/operations/local/index.ts | 4 ++-- .../{publishVersion.ts => restoreVersion.ts} | 4 ++-- .../{publishVersion.ts => restoreVersion.ts} | 4 ++-- .../{publishVersion.ts => restoreVersion.ts} | 4 ++-- src/globals/init.ts | 2 +- .../{publishVersion.ts => restoreVersion.ts} | 4 ++-- .../{publishVersion.ts => restoreVersion.ts} | 6 +++--- src/graphql/bindResolvers.ts | 6 +++--- src/index.ts | 17 ++++++----------- src/init/bindOperations.ts | 12 ++++++------ src/init/bindRequestHandlers.ts | 12 ++++++------ 16 files changed, 45 insertions(+), 50 deletions(-) rename src/collections/graphql/resolvers/{publishVersion.ts => restoreVersion.ts} (75%) rename src/collections/operations/local/{publishVersion.ts => restoreVersion.ts} (87%) rename src/collections/operations/{publishVersion.ts => restoreVersion.ts} (97%) rename src/collections/requestHandlers/{publishVersion.ts => restoreVersion.ts} (84%) rename src/globals/operations/{publishVersion.ts => restoreVersion.ts} (97%) rename src/globals/requestHandlers/{publishVersion.ts => restoreVersion.ts} (84%) diff --git a/docs/versions/drafts.mdx b/docs/versions/drafts.mdx index a08bf887fd..30f67d77f2 100644 --- a/docs/versions/drafts.mdx +++ b/docs/versions/drafts.mdx @@ -3,7 +3,7 @@ title: Drafts label: Drafts order: 20 desc: Enable drafts on collection documents or globals and build true preview environments for your data. -keywords: version history, drafts, preview, draft, publish, autosave, Content Management System, cms, headless, javascript, node, react, express +keywords: version history, drafts, preview, draft, restore, publish, autosave, Content Management System, cms, headless, javascript, node, react, express --- Payload's Draft functionality builds on top of the Versions functionality to allow you to make changes to your collection documents and globals, but publish only when you're ready. This functionality allows you to build powerful Preview environments for your data, where you can make sure your changes look good before publishing documents. diff --git a/docs/versions/overview.mdx b/docs/versions/overview.mdx index ab73cff6b8..b59fb925a6 100644 --- a/docs/versions/overview.mdx +++ b/docs/versions/overview.mdx @@ -3,7 +3,7 @@ title: Versions label: Overview order: 10 desc: Keep a running history or audit log of changes to collection documents and globals. -keywords: version history, revisions, audit log, draft, publish, autosave, Content Management System, cms, headless, javascript, node, react, express +keywords: version history, revisions, audit log, draft, publish, restore, autosave, Content Management System, cms, headless, javascript, node, react, express --- diff --git a/src/collections/graphql/init.ts b/src/collections/graphql/init.ts index a15a209684..92c521e475 100644 --- a/src/collections/graphql/init.ts +++ b/src/collections/graphql/init.ts @@ -15,7 +15,7 @@ import buildVersionWhereInputType from '../../graphql/schema/buildVersionWhereIn function registerCollections(): void { const { - findVersions, findVersionByID, publishVersion, + findVersions, findVersionByID, restoreVersion, create, find, findByID, deleteResolver, update, } = this.graphQL.resolvers.collections; @@ -206,7 +206,7 @@ function registerCollections(): void { args: { id: { type: GraphQLString }, }, - resolve: publishVersion(collection), + resolve: restoreVersion(collection), }; } diff --git a/src/collections/graphql/resolvers/publishVersion.ts b/src/collections/graphql/resolvers/restoreVersion.ts similarity index 75% rename from src/collections/graphql/resolvers/publishVersion.ts rename to src/collections/graphql/resolvers/restoreVersion.ts index 6e1a3cbbf4..e229eeed61 100644 --- a/src/collections/graphql/resolvers/publishVersion.ts +++ b/src/collections/graphql/resolvers/restoreVersion.ts @@ -15,7 +15,7 @@ export type Resolver = ( } ) => Promise -export default function publishVersion(collection: Collection): Resolver { +export default function restoreVersion(collection: Collection): Resolver { async function resolver(_, args, context) { if (args.locale) context.req.locale = args.locale; if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; @@ -26,10 +26,10 @@ export default function publishVersion(collection: Collection): Resolver { req: context.req, }; - await this.operations.collections.publishVersion(options); + await this.operations.collections.restoreVersion(options); return true; } - const findVersionByIDResolver = resolver.bind(this); - return findVersionByIDResolver; + const restoreVersionResolver = resolver.bind(this); + return restoreVersionResolver; } diff --git a/src/collections/init.ts b/src/collections/init.ts index 9ad618ea08..adbe2a0bb3 100644 --- a/src/collections/init.ts +++ b/src/collections/init.ts @@ -106,7 +106,7 @@ export default function registerCollections(ctx: Payload): void { findByID, findVersions, findVersionByID, - publishVersion, + restoreVersion, delete: deleteHandler, } = ctx.requestHandlers.collections; @@ -182,7 +182,7 @@ export default function registerCollections(ctx: Payload): void { router.route(`/${slug}/versions/:id`) .get(findVersionByID) - .post(publishVersion); + .post(restoreVersion); } router.route(`/${slug}`) diff --git a/src/collections/operations/local/index.ts b/src/collections/operations/local/index.ts index e13d251f54..c6cc8fe5f4 100644 --- a/src/collections/operations/local/index.ts +++ b/src/collections/operations/local/index.ts @@ -6,7 +6,7 @@ import localDelete from './delete'; import auth from '../../../auth/operations/local'; import findVersionByID from './findVersionByID'; import findVersions from './findVersions'; -import publishVersion from './publishVersion'; +import restoreVersion from './restoreVersion'; export default { find, @@ -17,5 +17,5 @@ export default { auth, findVersionByID, findVersions, - publishVersion, + restoreVersion, }; diff --git a/src/collections/operations/local/publishVersion.ts b/src/collections/operations/local/restoreVersion.ts similarity index 87% rename from src/collections/operations/local/publishVersion.ts rename to src/collections/operations/local/restoreVersion.ts index 352f9fe807..b01da6012a 100644 --- a/src/collections/operations/local/publishVersion.ts +++ b/src/collections/operations/local/restoreVersion.ts @@ -13,7 +13,7 @@ export type Options = { showHiddenFields?: boolean } -export default async function publishVersion = any>(options: Options): Promise { +export default async function restoreVersion = any>(options: Options): Promise { const { collection: collectionSlug, depth, @@ -44,5 +44,5 @@ export default async function publishVersion = any> }, }; - return this.operations.collections.publishVersion(args); + return this.operations.collections.restoreVersion(args); } diff --git a/src/collections/operations/publishVersion.ts b/src/collections/operations/restoreVersion.ts similarity index 97% rename from src/collections/operations/publishVersion.ts rename to src/collections/operations/restoreVersion.ts index 069bfaf077..1fde3d1705 100644 --- a/src/collections/operations/publishVersion.ts +++ b/src/collections/operations/restoreVersion.ts @@ -20,7 +20,7 @@ export type Arguments = { depth?: number } -async function publishVersion(this: Payload, args: Arguments): Promise { +async function restoreVersion(this: Payload, args: Arguments): Promise { const { collection: { Model, @@ -171,4 +171,4 @@ async function publishVersion(this: Payload, args: A return result; } -export default publishVersion; +export default restoreVersion; diff --git a/src/collections/requestHandlers/publishVersion.ts b/src/collections/requestHandlers/restoreVersion.ts similarity index 84% rename from src/collections/requestHandlers/publishVersion.ts rename to src/collections/requestHandlers/restoreVersion.ts index b16dbab6f3..fcf57ec49e 100644 --- a/src/collections/requestHandlers/publishVersion.ts +++ b/src/collections/requestHandlers/restoreVersion.ts @@ -9,7 +9,7 @@ export type RestoreResult = { doc: Document }; -export default async function publishVersion(req: PayloadRequest, res: Response, next: NextFunction): Promise | void> { +export default async function restoreVersion(req: PayloadRequest, res: Response, next: NextFunction): Promise | void> { const options = { req, collection: req.collection, @@ -18,7 +18,7 @@ export default async function publishVersion(req: PayloadRequest, res: Response, }; try { - const doc = await this.operations.collections.publishVersion(options); + const doc = await this.operations.collections.restoreVersion(options); return res.status(httpStatus.OK).json({ ...formatSuccessResponse('Restored successfully.', 'message'), doc, diff --git a/src/globals/init.ts b/src/globals/init.ts index f135565059..4d6ca309dc 100644 --- a/src/globals/init.ts +++ b/src/globals/init.ts @@ -54,7 +54,7 @@ export default function initGlobals(ctx: Payload): void { router.route(`/globals/${global.slug}/versions/:id`) .get(ctx.requestHandlers.globals.findVersionByID(global)) - .post(ctx.requestHandlers.globals.publishVersion(global)); + .post(ctx.requestHandlers.globals.restoreVersion(global)); } }); diff --git a/src/globals/operations/publishVersion.ts b/src/globals/operations/restoreVersion.ts similarity index 97% rename from src/globals/operations/publishVersion.ts rename to src/globals/operations/restoreVersion.ts index 413a2f1d2e..6162191b58 100644 --- a/src/globals/operations/publishVersion.ts +++ b/src/globals/operations/restoreVersion.ts @@ -23,7 +23,7 @@ export type Arguments = { // TODO: finish -async function publishVersion = any>(this: Payload, args: Arguments): Promise> { +async function restoreVersion = any>(this: Payload, args: Arguments): Promise> { const { globals: { Model } } = this; const { @@ -143,4 +143,4 @@ async function publishVersion = any>(this: Payload, return result; } -export default publishVersion; +export default restoreVersion; diff --git a/src/globals/requestHandlers/publishVersion.ts b/src/globals/requestHandlers/restoreVersion.ts similarity index 84% rename from src/globals/requestHandlers/publishVersion.ts rename to src/globals/requestHandlers/restoreVersion.ts index 046acdeb23..bb638bb97e 100644 --- a/src/globals/requestHandlers/publishVersion.ts +++ b/src/globals/requestHandlers/restoreVersion.ts @@ -15,7 +15,7 @@ export default function (globalConfig: SanitizedGlobalConfig) { }; try { - const doc = await this.operations.globals.publishVersion(options); + const doc = await this.operations.globals.restoreVersion(options); return res.status(httpStatus.OK).json({ ...formatSuccessResponse('Restored successfully.', 'message'), doc, @@ -25,6 +25,6 @@ export default function (globalConfig: SanitizedGlobalConfig) { } } - const publishVersionHandler = handler.bind(this); - return publishVersionHandler; + const restoreVersionHandler = handler.bind(this); + return restoreVersionHandler; } diff --git a/src/graphql/bindResolvers.ts b/src/graphql/bindResolvers.ts index 3d3920ce96..0f5d44a365 100644 --- a/src/graphql/bindResolvers.ts +++ b/src/graphql/bindResolvers.ts @@ -16,7 +16,7 @@ import update from '../collections/graphql/resolvers/update'; import deleteResolver from '../collections/graphql/resolvers/delete'; import findVersions from '../collections/graphql/resolvers/findVersions'; import findVersionByID from '../collections/graphql/resolvers/findVersionByID'; -import publishVersion from '../collections/graphql/resolvers/publishVersion'; +import restoreVersion from '../collections/graphql/resolvers/restoreVersion'; import findOne from '../globals/graphql/resolvers/findOne'; import globalUpdate from '../globals/graphql/resolvers/update'; @@ -30,7 +30,7 @@ export type GraphQLResolvers = { findVersions: typeof findVersions, findByID: typeof findByID, findVersionByID: typeof findVersionByID, - publishVersion: typeof publishVersion, + restoreVersion: typeof restoreVersion, update: typeof update, deleteResolver: typeof deleteResolver, auth: { @@ -61,7 +61,7 @@ function bindResolvers(ctx: Payload): void { findVersions: findVersions.bind(ctx), findByID: findByID.bind(ctx), findVersionByID: findVersionByID.bind(ctx), - publishVersion: publishVersion.bind(ctx), + restoreVersion: restoreVersion.bind(ctx), update: update.bind(ctx), deleteResolver: deleteResolver.bind(ctx), auth: { diff --git a/src/index.ts b/src/index.ts index cafa9d1b8b..4523887331 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,7 +48,7 @@ import { Options as UpdateOptions } from './collections/operations/local/update' import { Options as DeleteOptions } from './collections/operations/local/delete'; import { Options as FindVersionsOptions } from './collections/operations/local/findVersions'; import { Options as FindVersionByIDOptions } from './collections/operations/local/findVersionByID'; -import { Options as RestoreVersionOptions } from './collections/operations/local/publishVersion'; +import { Options as RestoreVersionOptions } from './collections/operations/local/restoreVersion'; import { Result } from './auth/operations/login'; require('isomorphic-fetch'); @@ -301,19 +301,14 @@ export class Payload { * @param options * @returns version with specified ID */ - publishVersion = async = any>(options: RestoreVersionOptions): Promise => { - let { publishVersion } = localOperations; - publishVersion = publishVersion.bind(this); - return publishVersion(options); + restoreVersion = async = any>(options: RestoreVersionOptions): Promise => { + let { restoreVersion } = localOperations; + restoreVersion = restoreVersion.bind(this); + return restoreVersion(options); } - // TODO: globals - // findVersionGlobal - // findVersionByIDGlobal - // publishVersionGlobal // TODO: - // graphql operations & request handlers, where - // tests + // graphql Global Versions login = async (options): Promise => { let { login } = localOperations.auth; diff --git a/src/init/bindOperations.ts b/src/init/bindOperations.ts index e1b1b7b3d0..a12a5bc86d 100644 --- a/src/init/bindOperations.ts +++ b/src/init/bindOperations.ts @@ -16,14 +16,14 @@ import find from '../collections/operations/find'; import findByID from '../collections/operations/findByID'; import findVersions from '../collections/operations/findVersions'; import findVersionByID from '../collections/operations/findVersionByID'; -import publishVersion from '../collections/operations/publishVersion'; +import restoreVersion from '../collections/operations/restoreVersion'; import update from '../collections/operations/update'; import deleteHandler from '../collections/operations/delete'; import findOne from '../globals/operations/findOne'; import findGlobalVersions from '../globals/operations/findVersions'; import findGlobalVersionByID from '../globals/operations/findVersionByID'; -import publishGlobalVersion from '../globals/operations/publishVersion'; +import restoreGlobalVersion from '../globals/operations/restoreVersion'; import globalUpdate from '../globals/operations/update'; import preferenceUpdate from '../preferences/operations/update'; @@ -37,7 +37,7 @@ export type Operations = { findByID: typeof findByID findVersions: typeof findVersions findVersionByID: typeof findVersionByID - publishVersion: typeof publishVersion + restoreVersion: typeof restoreVersion update: typeof update delete: typeof deleteHandler auth: { @@ -58,7 +58,7 @@ export type Operations = { findOne: typeof findOne findVersions: typeof findGlobalVersions findVersionByID: typeof findGlobalVersionByID - publishVersion: typeof publishGlobalVersion + restoreVersion: typeof restoreGlobalVersion update: typeof globalUpdate } preferences: { @@ -76,7 +76,7 @@ function bindOperations(ctx: Payload): void { findByID: findByID.bind(ctx), findVersions: findVersions.bind(ctx), findVersionByID: findVersionByID.bind(ctx), - publishVersion: publishVersion.bind(ctx), + restoreVersion: restoreVersion.bind(ctx), update: update.bind(ctx), delete: deleteHandler.bind(ctx), auth: { @@ -97,7 +97,7 @@ function bindOperations(ctx: Payload): void { findOne: findOne.bind(ctx), findVersions: findGlobalVersions.bind(ctx), findVersionByID: findGlobalVersionByID.bind(ctx), - publishVersion: publishGlobalVersion.bind(ctx), + restoreVersion: restoreGlobalVersion.bind(ctx), update: globalUpdate.bind(ctx), }, preferences: { diff --git a/src/init/bindRequestHandlers.ts b/src/init/bindRequestHandlers.ts index 8cff73ee61..f557f456f8 100644 --- a/src/init/bindRequestHandlers.ts +++ b/src/init/bindRequestHandlers.ts @@ -15,14 +15,14 @@ import find from '../collections/requestHandlers/find'; import findByID from '../collections/requestHandlers/findByID'; import findVersions from '../collections/requestHandlers/findVersions'; import findVersionByID from '../collections/requestHandlers/findVersionByID'; -import publishVersion from '../collections/requestHandlers/publishVersion'; +import restoreVersion from '../collections/requestHandlers/restoreVersion'; import update from '../collections/requestHandlers/update'; import deleteHandler from '../collections/requestHandlers/delete'; import findOne from '../globals/requestHandlers/findOne'; import findGlobalVersions from '../globals/requestHandlers/findVersions'; import findGlobalVersionByID from '../globals/requestHandlers/findVersionByID'; -import publishGlobalVersion from '../globals/requestHandlers/publishVersion'; +import restoreGlobalVersion from '../globals/requestHandlers/restoreVersion'; import globalUpdate from '../globals/requestHandlers/update'; import { Payload } from '../index'; import preferenceUpdate from '../preferences/requestHandlers/update'; @@ -36,7 +36,7 @@ export type RequestHandlers = { findByID: typeof findByID, findVersions: typeof findVersions findVersionByID: typeof findVersionByID, - publishVersion: typeof publishVersion, + restoreVersion: typeof restoreVersion, update: typeof update, delete: typeof deleteHandler, auth: { @@ -58,7 +58,7 @@ export type RequestHandlers = { update: typeof globalUpdate, findVersions: typeof findGlobalVersions findVersionByID: typeof findGlobalVersionByID - publishVersion: typeof publishGlobalVersion + restoreVersion: typeof restoreGlobalVersion }, preferences: { update: typeof preferenceUpdate, @@ -75,7 +75,7 @@ function bindRequestHandlers(ctx: Payload): void { findByID: findByID.bind(ctx), findVersions: findVersions.bind(ctx), findVersionByID: findVersionByID.bind(ctx), - publishVersion: publishVersion.bind(ctx), + restoreVersion: restoreVersion.bind(ctx), update: update.bind(ctx), delete: deleteHandler.bind(ctx), auth: { @@ -97,7 +97,7 @@ function bindRequestHandlers(ctx: Payload): void { update: globalUpdate.bind(ctx), findVersions: findGlobalVersions.bind(ctx), findVersionByID: findGlobalVersionByID.bind(ctx), - publishVersion: publishGlobalVersion.bind(ctx), + restoreVersion: restoreGlobalVersion.bind(ctx), }, preferences: { update: preferenceUpdate.bind(ctx), From 26b13a81c35be473367b01a317e10b0c84e0b5ee Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Fri, 11 Feb 2022 13:39:17 -0500 Subject: [PATCH 4/5] feat: improve code coverage for graphql versions --- src/collections/graphql/init.ts | 11 ++++- src/graphql/index.ts | 2 +- src/graphql/schema/buildInputObject.ts | 5 -- src/graphql/schema/buildVersionType.ts | 14 ++++-- .../schema/buildVersionWhereInputType.ts | 4 +- src/graphql/schema/buildWhereInputType.ts | 8 ++-- src/graphql/schema/fieldToSchemaMap.ts | 8 ++-- src/graphql/schema/operators.ts | 4 +- .../schema/recursivelyBuildNestedPaths.ts | 6 ++- src/versions/tests/graphql.spec.ts | 47 ++++++++++++++++--- 10 files changed, 81 insertions(+), 28 deletions(-) diff --git a/src/collections/graphql/init.ts b/src/collections/graphql/init.ts index 92c521e475..452fd1aa99 100644 --- a/src/collections/graphql/init.ts +++ b/src/collections/graphql/init.ts @@ -182,11 +182,15 @@ function registerCollections(): void { }; if (collection.config.versions) { - collection.graphQL.versionType = this.buildVersionType(collection.graphQL.type); + collection.graphQL.versionType = this.buildVersionType(collection.graphQL.type, collection.config.versions); this.Query.fields[`version${formatName(singularLabel)}`] = { type: collection.graphQL.versionType, args: { id: { type: GraphQLString }, + ...(this.config.localization ? { + locale: { type: this.types.localeInputType }, + fallbackLocale: { type: this.types.fallbackLocaleInputType }, + } : {}), }, resolve: findVersionByID(collection), }; @@ -194,7 +198,10 @@ function registerCollections(): void { type: buildPaginatedListType(`versions${formatName(pluralLabel)}`, collection.graphQL.versionType), args: { where: { type: buildVersionWhereInputType(singularLabel, collection.config) }, - autosave: { type: GraphQLBoolean }, + ...(this.config.localization ? { + locale: { type: this.types.localeInputType }, + fallbackLocale: { type: this.types.fallbackLocaleInputType }, + } : {}), page: { type: GraphQLInt }, limit: { type: GraphQLInt }, sort: { type: GraphQLString }, diff --git a/src/graphql/index.ts b/src/graphql/index.ts index a4ae2b029f..3478d11877 100644 --- a/src/graphql/index.ts +++ b/src/graphql/index.ts @@ -14,7 +14,7 @@ import initGlobals from '../globals/graphql/init'; import initPreferences from '../preferences/graphql/init'; import { GraphQLResolvers } from './bindResolvers'; import buildVersionType from './schema/buildVersionType'; -import { buildWhereInputType } from './schema/buildWhereInputType'; +import buildWhereInputType from './schema/buildWhereInputType'; import { SanitizedConfig } from '../config/types'; type GraphQLTypes = { diff --git a/src/graphql/schema/buildInputObject.ts b/src/graphql/schema/buildInputObject.ts index d415cd0ee7..cc453fc451 100644 --- a/src/graphql/schema/buildInputObject.ts +++ b/src/graphql/schema/buildInputObject.ts @@ -1,9 +1,7 @@ import { GraphQLInputFieldConfigMap, GraphQLInputObjectType, - GraphQLInt, GraphQLList, - GraphQLString, Thunk, } from 'graphql'; @@ -24,9 +22,6 @@ const buildInputObject = (name: string, fieldTypes: Thunk { + const autosave = (versionsConfig.drafts && versionsConfig.drafts?.autosave && { autosave: { type: GraphQLBoolean } }); -const buildVersionType = (type: GraphQLObjectType): GraphQLObjectType => { return new GraphQLObjectType({ name: formatName(`${type.name}Version`), fields: { id: { type: GraphQLString }, parent: { type: GraphQLString }, - autosave: { type: GraphQLBoolean }, version: { type }, updatedAt: { type: new GraphQLNonNull(DateTimeResolver) }, createdAt: { type: new GraphQLNonNull(DateTimeResolver) }, + ...autosave, }, }); }; diff --git a/src/graphql/schema/buildVersionWhereInputType.ts b/src/graphql/schema/buildVersionWhereInputType.ts index 2cbb4f471c..f4a0de2665 100644 --- a/src/graphql/schema/buildVersionWhereInputType.ts +++ b/src/graphql/schema/buildVersionWhereInputType.ts @@ -9,9 +9,9 @@ import formatName from '../utilities/formatName'; import withOperators from './withOperators'; import { FieldAffectingData } from '../../fields/config/types'; import buildInputObject from './buildInputObject'; -import { operators } from './operators'; +import operators from './operators'; import { SanitizedCollectionConfig } from '../../collections/config/types'; -import { recursivelyBuildNestedPaths } from './recursivelyBuildNestedPaths'; +import recursivelyBuildNestedPaths from './recursivelyBuildNestedPaths'; const buildVersionWhereInputType = (singularLabel: string, parentCollection: SanitizedCollectionConfig): GraphQLInputObjectType => { const name = `version${formatName(singularLabel)}`; diff --git a/src/graphql/schema/buildWhereInputType.ts b/src/graphql/schema/buildWhereInputType.ts index bf9c33fda7..98be8366c1 100644 --- a/src/graphql/schema/buildWhereInputType.ts +++ b/src/graphql/schema/buildWhereInputType.ts @@ -14,9 +14,9 @@ import { } from '../../fields/config/types'; import formatName from '../utilities/formatName'; import withOperators from './withOperators'; -import { operators } from './operators'; +import operators from './operators'; import buildInputObject from './buildInputObject'; -import { fieldToSchemaMap } from './fieldToSchemaMap'; +import fieldToSchemaMap from './fieldToSchemaMap'; // buildWhereInputType is similar to buildObjectType and operates // on a field basis with a few distinct differences. @@ -25,7 +25,7 @@ import { fieldToSchemaMap } from './fieldToSchemaMap'; // 2. Relationships, groups, repeaters and flex content are not // directly searchable. Instead, we need to build a chained pathname // using dot notation so Mongo can properly search nested paths. -export const buildWhereInputType = (name: string, fields: Field[], parentName: string): GraphQLInputObjectType => { +const buildWhereInputType = (name: string, fields: Field[], parentName: string): GraphQLInputObjectType => { // This is the function that builds nested paths for all // field types with nested paths. @@ -69,3 +69,5 @@ export const buildWhereInputType = (name: string, fields: Field[], parentName: s return buildInputObject(fieldName, fieldTypes); }; + +export default buildWhereInputType; diff --git a/src/graphql/schema/fieldToSchemaMap.ts b/src/graphql/schema/fieldToSchemaMap.ts index 39f330751d..7c9d5a1ccf 100644 --- a/src/graphql/schema/fieldToSchemaMap.ts +++ b/src/graphql/schema/fieldToSchemaMap.ts @@ -20,12 +20,12 @@ import { TextField, UploadField, } from '../../fields/config/types'; import withOperators from './withOperators'; -import { operators } from './operators'; +import operators from './operators'; import combineParentName from '../utilities/combineParentName'; import formatName from '../utilities/formatName'; -import { recursivelyBuildNestedPaths } from './recursivelyBuildNestedPaths'; +import recursivelyBuildNestedPaths from './recursivelyBuildNestedPaths'; -export const fieldToSchemaMap = (parentName: string) => ({ +const fieldToSchemaMap: (parentName: string) => any = (parentName: string) => ({ number: (field: NumberField) => { const type = GraphQLFloat; return { @@ -254,3 +254,5 @@ export const fieldToSchemaMap = (parentName: string) => ({ return rowSchema; }, []), }); + +export default fieldToSchemaMap; diff --git a/src/graphql/schema/operators.ts b/src/graphql/schema/operators.ts index b13764d830..b59112e030 100644 --- a/src/graphql/schema/operators.ts +++ b/src/graphql/schema/operators.ts @@ -1,6 +1,8 @@ -export const operators = { +const operators = { equality: ['equals', 'not_equals'], contains: ['in', 'not_in', 'all'], comparison: ['greater_than_equal', 'greater_than', 'less_than_equal', 'less_than'], geo: ['near'], }; + +export default operators; diff --git a/src/graphql/schema/recursivelyBuildNestedPaths.ts b/src/graphql/schema/recursivelyBuildNestedPaths.ts index 572899111a..bca906b772 100644 --- a/src/graphql/schema/recursivelyBuildNestedPaths.ts +++ b/src/graphql/schema/recursivelyBuildNestedPaths.ts @@ -4,9 +4,9 @@ import { fieldIsPresentationalOnly, FieldWithSubFields, } from '../../fields/config/types'; -import { fieldToSchemaMap } from './fieldToSchemaMap'; +import fieldToSchemaMap from './fieldToSchemaMap'; -export const recursivelyBuildNestedPaths = (parentName: string, field: FieldWithSubFields & FieldAffectingData) => { +const recursivelyBuildNestedPaths = (parentName: string, field: FieldWithSubFields & FieldAffectingData) => { const nestedPaths = field.fields.reduce((nestedFields, nestedField) => { if (!fieldIsPresentationalOnly(nestedField)) { const getFieldSchema = fieldToSchemaMap(parentName)[nestedField.type]; @@ -40,3 +40,5 @@ export const recursivelyBuildNestedPaths = (parentName: string, field: FieldWith return nestedPaths; }; + +export default recursivelyBuildNestedPaths; diff --git a/src/versions/tests/graphql.spec.ts b/src/versions/tests/graphql.spec.ts index 350e2ed79a..ff35f1a689 100644 --- a/src/versions/tests/graphql.spec.ts +++ b/src/versions/tests/graphql.spec.ts @@ -64,23 +64,58 @@ describe('GrahpQL Version Resolvers', () => { }); describe('Read', () => { - it('should allow read of autosavePost versions', async () => { - const updatedTitle = 'updated title'; + const updatedTitle = 'updated title'; - // modify the post so it will create a new version + beforeAll(async (done) => { + // modify the post to create a new version // language=graphQL const update = `mutation { updateAutosavePost(id: "${postID}", data: {title: "${updatedTitle}"}) { - title + title } }`; - await client.request(update); - // query the version // language=graphQL const query = `query { versionsAutosavePosts(where: { parent: { equals: "${postID}" } }) { + docs { + id + } + } + }`; + + const response = await client.request(query); + + versionID = response.versionsAutosavePosts.docs[0].id; + done(); + }); + + it('should allow read of versions by version id', async () => { + const query = `query { + versionAutosavePost(id: "${versionID}") { + id + parent + version { + title + } + } + }`; + + const response = await client.request(query); + + const data = response.versionAutosavePost; + versionID = data.id; + + expect(data.id).toBeDefined(); + expect(data.parent).toStrictEqual(postID); + expect(data.version.title).toStrictEqual(title); + }); + + it('should allow read of versions by querying version content', async () => { + // language=graphQL + const query = `query { + versionsAutosavePosts(where: { version__title: {equals: "${title}" } }) { docs { id parent From 407bc35a316bd07b463391ad03e201b3a40847bf Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Fri, 11 Feb 2022 13:45:27 -0500 Subject: [PATCH 5/5] chore: rename buildInputObject to withWhereAndOr --- src/graphql/schema/buildVersionWhereInputType.ts | 4 ++-- src/graphql/schema/buildWhereInputType.ts | 4 ++-- src/graphql/schema/{buildInputObject.ts => withWhereAndOr.ts} | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/graphql/schema/{buildInputObject.ts => withWhereAndOr.ts} (78%) diff --git a/src/graphql/schema/buildVersionWhereInputType.ts b/src/graphql/schema/buildVersionWhereInputType.ts index f4a0de2665..9df17133d2 100644 --- a/src/graphql/schema/buildVersionWhereInputType.ts +++ b/src/graphql/schema/buildVersionWhereInputType.ts @@ -8,7 +8,7 @@ import { GraphQLJSON } from 'graphql-type-json'; import formatName from '../utilities/formatName'; import withOperators from './withOperators'; import { FieldAffectingData } from '../../fields/config/types'; -import buildInputObject from './buildInputObject'; +import withWhereAndOr from './withWhereAndOr'; import operators from './operators'; import { SanitizedCollectionConfig } from '../../collections/config/types'; import recursivelyBuildNestedPaths from './recursivelyBuildNestedPaths'; @@ -41,7 +41,7 @@ const buildVersionWhereInputType = (singularLabel: string, parentCollection: San fieldTypes[versionField.key] = versionField.type; }); - return buildInputObject(name, fieldTypes); + return withWhereAndOr(name, fieldTypes); }; export default buildVersionWhereInputType; diff --git a/src/graphql/schema/buildWhereInputType.ts b/src/graphql/schema/buildWhereInputType.ts index 98be8366c1..ace27b8e02 100644 --- a/src/graphql/schema/buildWhereInputType.ts +++ b/src/graphql/schema/buildWhereInputType.ts @@ -15,7 +15,7 @@ import { import formatName from '../utilities/formatName'; import withOperators from './withOperators'; import operators from './operators'; -import buildInputObject from './buildInputObject'; +import withWhereAndOr from './withWhereAndOr'; import fieldToSchemaMap from './fieldToSchemaMap'; // buildWhereInputType is similar to buildObjectType and operates @@ -67,7 +67,7 @@ const buildWhereInputType = (name: string, fields: Field[], parentName: string): const fieldName = formatName(name); - return buildInputObject(fieldName, fieldTypes); + return withWhereAndOr(fieldName, fieldTypes); }; export default buildWhereInputType; diff --git a/src/graphql/schema/buildInputObject.ts b/src/graphql/schema/withWhereAndOr.ts similarity index 78% rename from src/graphql/schema/buildInputObject.ts rename to src/graphql/schema/withWhereAndOr.ts index cc453fc451..b5925123b1 100644 --- a/src/graphql/schema/buildInputObject.ts +++ b/src/graphql/schema/withWhereAndOr.ts @@ -5,7 +5,7 @@ import { Thunk, } from 'graphql'; -const buildInputObject = (name: string, fieldTypes: Thunk): GraphQLInputObjectType => { +const withWhereAndOr = (name: string, fieldTypes: Thunk): GraphQLInputObjectType => { return new GraphQLInputObjectType({ name: `${name}_where`, fields: { @@ -26,4 +26,4 @@ const buildInputObject = (name: string, fieldTypes: Thunk