Merge branch 'feat/graphql-versions' of github.com:payloadcms/payload into feat/graphql-versions

This commit is contained in:
James
2022-02-11 14:50:48 -05:00
29 changed files with 789 additions and 369 deletions

View File

@@ -11,9 +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 {
findVersions, findVersionByID, restoreVersion,
create, find, findByID, deleteResolver, update,
} = this.graphQL.resolvers.collections;
@@ -179,6 +181,42 @@ function registerCollections(): void {
resolve: deleteResolver(collection),
};
if (collection.config.versions) {
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),
};
this.Query.fields[`versions${pluralLabel}`] = {
type: buildPaginatedListType(`versions${formatName(pluralLabel)}`, collection.graphQL.versionType),
args: {
where: { type: buildVersionWhereInputType(singularLabel, collection.config) },
...(this.config.localization ? {
locale: { type: this.types.localeInputType },
fallbackLocale: { type: this.types.fallbackLocaleInputType },
} : {}),
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: restoreVersion(collection),
};
}
if (collection.config.auth) {
collection.graphQL.JWT = this.buildObjectType(
formatName(`${slug}JWT`),

View File

@@ -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<Document>
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;
}

View File

@@ -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<Document>
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;
}

View File

@@ -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<Document>
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;
const options = {
collection,
id: args.id,
req: context.req,
};
await this.operations.collections.restoreVersion(options);
return true;
}
const restoreVersionResolver = resolver.bind(this);
return restoreVersionResolver;
}

View File

@@ -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}`)

View File

@@ -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,
};

View File

@@ -13,7 +13,7 @@ export type Options = {
showHiddenFields?: boolean
}
export default async function publishVersion<T extends TypeWithVersion<T> = any>(options: Options): Promise<T> {
export default async function restoreVersion<T extends TypeWithVersion<T> = any>(options: Options): Promise<T> {
const {
collection: collectionSlug,
depth,
@@ -44,5 +44,5 @@ export default async function publishVersion<T extends TypeWithVersion<T> = any>
},
};
return this.operations.collections.publishVersion(args);
return this.operations.collections.restoreVersion(args);
}

View File

@@ -20,7 +20,7 @@ export type Arguments = {
depth?: number
}
async function publishVersion<T extends TypeWithID = any>(this: Payload, args: Arguments): Promise<T> {
async function restoreVersion<T extends TypeWithID = any>(this: Payload, args: Arguments): Promise<T> {
const {
collection: {
Model,
@@ -171,4 +171,4 @@ async function publishVersion<T extends TypeWithID = any>(this: Payload, args: A
return result;
}
export default publishVersion;
export default restoreVersion;

View File

@@ -9,7 +9,7 @@ export type RestoreResult = {
doc: Document
};
export default async function publishVersion(req: PayloadRequest, res: Response, next: NextFunction): Promise<Response<RestoreResult> | void> {
export default async function restoreVersion(req: PayloadRequest, res: Response, next: NextFunction): Promise<Response<RestoreResult> | 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,

View File

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

View File

@@ -23,7 +23,7 @@ export type Arguments = {
// TODO: finish
async function publishVersion<T extends TypeWithVersion<T> = any>(this: Payload, args: Arguments): Promise<PaginatedDocs<T>> {
async function restoreVersion<T extends TypeWithVersion<T> = any>(this: Payload, args: Arguments): Promise<PaginatedDocs<T>> {
const { globals: { Model } } = this;
const {
@@ -143,4 +143,4 @@ async function publishVersion<T extends TypeWithVersion<T> = any>(this: Payload,
return result;
}
export default publishVersion;
export default restoreVersion;

View File

@@ -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;
}

View File

@@ -14,6 +14,9 @@ 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 restoreVersion from '../collections/graphql/resolvers/restoreVersion';
import findOne from '../globals/graphql/resolvers/findOne';
import globalUpdate from '../globals/graphql/resolvers/update';
@@ -24,7 +27,10 @@ export type GraphQLResolvers = {
collections: {
create: typeof create,
find: typeof find,
findVersions: typeof findVersions,
findByID: typeof findByID,
findVersionByID: typeof findVersionByID,
restoreVersion: typeof restoreVersion,
update: typeof update,
deleteResolver: typeof deleteResolver,
auth: {
@@ -52,7 +58,10 @@ 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),
restoreVersion: restoreVersion.bind(ctx),
update: update.bind(ctx),
deleteResolver: deleteResolver.bind(ctx),
auth: {

View File

@@ -13,6 +13,7 @@ import initCollections from '../collections/graphql/init';
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 { SanitizedConfig } from '../config/types';
@@ -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);

View File

@@ -0,0 +1,27 @@
import {
GraphQLBoolean,
GraphQLNonNull,
GraphQLObjectType,
GraphQLString,
} from 'graphql';
import { DateTimeResolver } from 'graphql-scalars';
import formatName from '../utilities/formatName';
import { SanitizedCollectionVersions } from '../../versions/types';
const buildVersionType = (type: GraphQLObjectType, versionsConfig: SanitizedCollectionVersions): GraphQLObjectType => {
const autosave = (versionsConfig.drafts && versionsConfig.drafts?.autosave && { autosave: { type: GraphQLBoolean } });
return new GraphQLObjectType({
name: formatName(`${type.name}Version`),
fields: {
id: { type: GraphQLString },
parent: { type: GraphQLString },
version: { type },
updatedAt: { type: new GraphQLNonNull(DateTimeResolver) },
createdAt: { type: new GraphQLNonNull(DateTimeResolver) },
...autosave,
},
});
};
export default buildVersionType;

View File

@@ -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 withWhereAndOr from './withWhereAndOr';
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 withWhereAndOr(name, fieldTypes);
};
export default buildVersionWhereInputType;

View File

@@ -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 withWhereAndOr from './withWhereAndOr';
import fieldToSchemaMap from './fieldToSchemaMap';
// buildWhereInputType is similar to buildObjectType and operates
// on a field basis with a few distinct differences.
@@ -52,281 +28,10 @@ import withOperators from './withOperators';
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,7 @@ 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 withWhereAndOr(fieldName, fieldTypes);
};
export default buildWhereInputType;

View File

@@ -0,0 +1,258 @@
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';
const fieldToSchemaMap: (parentName: string) => any = (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;
}, []),
});
export default fieldToSchemaMap;

View File

@@ -0,0 +1,8 @@
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;

View File

@@ -0,0 +1,44 @@
import {
FieldAffectingData,
fieldAffectsData,
fieldIsPresentationalOnly,
FieldWithSubFields,
} from '../../fields/config/types';
import fieldToSchemaMap from './fieldToSchemaMap';
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;
};
export default recursivelyBuildNestedPaths;

View File

@@ -0,0 +1,29 @@
import {
GraphQLInputFieldConfigMap,
GraphQLInputObjectType,
GraphQLList,
Thunk,
} from 'graphql';
const withWhereAndOr = (name: string, fieldTypes: Thunk<GraphQLInputFieldConfigMap>): 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,
})),
},
},
});
};
export default withWhereAndOr;

View File

@@ -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 <T extends TypeWithVersion<T> = any>(options: RestoreVersionOptions): Promise<T> => {
let { publishVersion } = localOperations;
publishVersion = publishVersion.bind(this);
return publishVersion(options);
restoreVersion = async <T extends TypeWithVersion<T> = any>(options: RestoreVersionOptions): Promise<T> => {
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 <T extends TypeWithID = any>(options): Promise<Result & { user: T}> => {
let { login } = localOperations.auth;

View File

@@ -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: {

View File

@@ -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),

View File

@@ -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'

View File

@@ -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 () => {

View File

@@ -0,0 +1,161 @@
/**
* @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 Version Resolvers', () => {
const title = 'autosave title';
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 description = 'autosave description';
const query = `mutation {
createAutosavePost(data: {title: "${title}", description: "${description}"}) {
id
title
description
createdAt
updatedAt
_status
}
}`;
const response = await client.request(query);
const data = response.createAutosavePost;
postID = data.id;
expect(data._status).toStrictEqual('draft');
});
});
describe('Read', () => {
const updatedTitle = 'updated title';
beforeAll(async (done) => {
// modify the post to create a new version
// language=graphQL
const update = `mutation {
updateAutosavePost(id: "${postID}", data: {title: "${updatedTitle}"}) {
title
}
}`;
await client.request(update);
// 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
version {
title
}
}
}
}`;
const response = await client.request(query);
const data = response.versionsAutosavePosts;
const doc = data.docs[0];
versionID = doc.id;
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 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);
});
});
});