import { model, Schema, nSchemaType, Document, Types } from 'mongoose'; import { Enum } from '../../library/enum'; import { ingredientSchema, IngredientProps } from './ingredient-model'; import { Ingredient } from './ingredient-model'; import { MongooseAdapter } from 'factory-girl'; import { isIngredientGroup, isIngredientItem } from './utils'; import { UserInputError } from 'apollo-server-micro'; // // Declarations // export const IngredientListItemKind = Enum({ GROUP: 'group', SINGLE: 'single', }); export type IngredientListItemKind = Enum; // ** Gerneric Ingredient List Item export interface ILItemBaseProps { kind: IngredientListItemKind; } // ** Single Ingredient List Item export interface IngredientItemProps extends ILItemBaseProps { node: IngredientProps; } export interface IngredientItem extends ILItemBaseProps, Types.Embedded { node: Ingredient; } // ** Group Ingredient List Item export interface IngredientGroupProps extends ILItemBaseProps { heading: string; nodes: IngredientProps[]; } export interface IngredientGroup extends ILItemBaseProps, Types.Embedded { heading: string; nodes: Types.DocumentArray; } // All possilbe items within an IngredientList export type IngredientListItem = IngredientGroup | IngredientItem; // ** Ingredient List export interface IngredientListProps { nodes: IngredientListItem[]; } export interface IngredientList extends Document { nodes: Types.DocumentArray; addIngredient: ( props: IngredientProps, options: { order: number; groupId: string } ) => IngredientItem; updateIngredient: ( id: string, props: Partial, options: { order: number; groupId: string } ) => IngredientItem; addGroup: ( props: { heading?: string }, options?: { order?: number } ) => IngredientGroup; updateGroup: ( id: string, props?: { heading?: string }, options?: { order?: number } ) => IngredientGroup; removeIngredient: (id: string) => IngredientItem; removeGroup: (id: string) => IngredientGroup; findDeep: (id: string) => null | IngredientItem; } // // Schemas // // ** Generic Ingredient List Item export const IngredientListItemSchema = new Schema( {}, { discriminatorKey: 'kind' } ); // ** Single Ingredient List Item export const IngredientItemSchema = new Schema({ node: ingredientSchema, }); // ** Group Ingredient List Item export const IngredientGroupSchema = new Schema({ heading: { type: String, required: true }, nodes: [IngredientListItemSchema], }); // ** Ingredient List export const IngredientListSchema = new Schema({ nodes: [IngredientListItemSchema], }); /** * Discriminators * * Use discriminators to allow IngredientList to contain either a single * ingredient, or a ingredientGroup * see: http://mongoosejs.com/docs/discriminators.html#embedded-discriminators-in-arrays */ const ingredientListNodes = IngredientListSchema.path('nodes') as nSchemaType; export const IngredientListGroup = ingredientListNodes.discriminator< IngredientGroup >('group', IngredientGroupSchema); export const IngredientListSingle = ingredientListNodes.discriminator< IngredientItem >('single', IngredientItemSchema); /** Nested IngredientListSingle within the IngredientGroup nodes array */ (IngredientGroupSchema.path('nodes') as nSchemaType).discriminator( 'single', IngredientItemSchema ); // // Model // // Custom Methods IngredientListSchema.methods = { /** * Add a recipe to the ingredientList */ addIngredient( this: IngredientList, props: IngredientProps, options?: { order?: number; groupId?: string } ) { const { order, groupId } = options; let ingredientGroup = null; const ingredient = new Ingredient(props); const ingredientItem = new IngredientListSingle({ type: IngredientListItemKind.SINGLE, node: ingredient, }); if (groupId) { ingredientGroup = this.nodes.id(groupId); if (!isIngredientGroup(ingredientGroup)) { throw new UserInputError('Invalid group'); } } const parentArray = ingredientGroup ? ingredientGroup.nodes : this.nodes; if (order && order < parentArray.length) { parentArray.splice(order, 0, ingredientItem); } else { parentArray.push(ingredientItem); } return ingredientItem; }, updateIngredient( this: IngredientList, id: string, props: Partial, options?: { order?: number; groupId?: string } ) { const { order, groupId } = options; const ingredientSingle = this.findDeep(id); const currentParent = ingredientSingle.parent() as | IngredientGroup | IngredientList; const newGroup = groupId && groupId !== currentParent.id ? this.nodes.id(groupId) : null; if (ingredientSingle === null || !isIngredientItem(ingredientSingle)) { throw new UserInputError('Ingredient Not Found'); } if (groupId && !isIngredientGroup(newGroup)) { throw new UserInputError('Group Not Found'); } let newParent = currentParent; if (groupId === null) { newParent = this; } if (newGroup && isIngredientGroup(newGroup)) { newParent = newGroup; } // move order or group if (newGroup || order !== undefined) { currentParent.nodes.pull(ingredientSingle.id); if (order !== undefined) { newParent.nodes.splice(order, 0, ingredientSingle); } else { newParent.nodes.push(ingredientSingle); } } // update values if (props) { ingredientSingle.node.set(props); } return ingredientSingle; }, /** * Remoe a recipe to the ingredientList */ removeIngredient(this: IngredientList, id: string) { const ingredient = this.findDeep(id); if (ingredient === null || !isIngredientItem(ingredient)) { throw new UserInputError('Ingredient Not Found'); } ingredient.remove(); return ingredient; }, /** * Add a group to the ingredientList */ addGroup( this: IngredientGroup, props: { heading?: string }, options?: { order?: number } ) { const { order } = options; const ingredientGroup = new IngredientListGroup({ type: IngredientListItemKind.GROUP, ...props, }); if (order && order < this.nodes.length) { this.nodes.splice(order, 0, ingredientGroup); } else { this.nodes.push(ingredientGroup); } return ingredientGroup; }, /** * Update a group to the ingredientList */ updateGroup( this: IngredientGroup, id: string, props?: { heading?: string }, options?: { order?: number } ) { const ingredientGroup = this.nodes.id(id); const { order } = options; if (ingredientGroup === null || !isIngredientGroup(ingredientGroup)) { throw new UserInputError('Unable to Find Group'); } // move order if (order !== undefined && order < this.nodes.length) { this.nodes.pull(ingredientGroup.id); this.nodes.splice(order, 0, ingredientGroup); } // update values ingredientGroup.set(props); return ingredientGroup; }, /** * Remove a group to the ingredientList */ removeGroup(this: IngredientList, id: string) { const ingredientGroup = this.nodes.id(id); if (ingredientGroup === null || !isIngredientGroup(ingredientGroup)) { throw new UserInputError('IngredientGroup Not Found'); } ingredientGroup.remove(); return ingredientGroup; }, /** * Find an ingredientItem within an IngredientList, even if nested inside * an IngredientGroup * * @param list The list you are searching within * @param id The id of the IngredientItem you are looking for */ findDeep(this: IngredientList, id: string) { // search top level first let ing = this.nodes.id(id); if (ing) return ing; // search within groups const ingredientGroups = this.nodes.filter(isIngredientGroup); for (let i = 0; i < ingredientGroups.length; i++) { ing = ingredientGroups[i].nodes.id(id); if (ing !== null) break; } return ing; }, }; export const IngredientList = model( 'IngredientList', IngredientListSchema );