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); + }); + }); });