Prechádzať zdrojové kódy

Add ingredient suggestions

Tatiana Inama 7 rokov pred
rodič
commit
70726219a3

+ 5 - 1
ktchn/src/ingredients/controller.ts

@@ -5,6 +5,7 @@ import { Request, Response, NextFunction } from 'express';
 const create = (db: IMongoService) => (req: Request, res: Response, next: NextFunction) => {
   const ingredients: Ingredient[] = [{
     name: 'all purpose flour',
+    variants: ['plain flour', 'all purpose flour', 'regular flour'],
     equivalences: {
       cup: 1,
       gr: 125,
@@ -18,6 +19,7 @@ const create = (db: IMongoService) => (req: Request, res: Response, next: NextFu
     }
   }, {
     name: 'bread flour',
+    variants: ['bread flour'],
     equivalences: {
       cup: 1,
       gr: 127,
@@ -31,6 +33,7 @@ const create = (db: IMongoService) => (req: Request, res: Response, next: NextFu
     }
   }, {
     name: 'pastry flour',
+    variants: ['pastry flour'],
     equivalences: {
       cup: 1,
       gr: 114,
@@ -107,6 +110,7 @@ const create = (db: IMongoService) => (req: Request, res: Response, next: NextFu
     prefferedUnit: 'gr',
   }, {
     name: 'short rice',
+    variants: ['sushi rice'],
     equivalences: {
       cup: 1,
       gr: 200,
@@ -127,7 +131,7 @@ const create = (db: IMongoService) => (req: Request, res: Response, next: NextFu
     prefferedUnit: 'gr',
   }, {
     name: 'granulated sugar',
-    variants: ['sugar'],
+    variants: ['sugar', 'plain sugar'],
     equivalences: {
       cup: 1,
       gr: 200,

+ 1 - 0
ktchn/src/ingredients/model.ts

@@ -12,6 +12,7 @@ type Equivalences = {
 };
 
 export interface Ingredient {
+  _id?: ObjectID,
   name: string,
   variants?: string[],
   equivalences: Equivalences,

+ 5 - 3
ktchn/src/mongo.ts

@@ -1,15 +1,17 @@
 import { Db, Cursor, MongoClient, InsertOneWriteOpResult, Collection, ObjectID, FilterQuery, ObjectId } from 'mongodb';
 
-export interface IDBDocument<T extends {}> {
+export interface IDDocument {
   _id: ObjectID
 }
 
+export type IDBDocument<T> = IDDocument & T;
+
 export interface IMongoService {
   insertOne<T>(data: T): Promise<IDBDocument<T>>,
   insertMany<T>(data: T[]): Promise<IDBDocument<T>[]|any[]>,
   findOne<T>(query: FilterQuery<T>): Promise<IDBDocument<T>>,
   findOneById<T>(idParam: string): Promise<IDBDocument<T>>,
-  find<T>(query: FilterQuery<T>): Promise<IDBDocument<T>[]>,
+  find<T>(query: FilterQuery<T>, optionalQuery?: FilterQuery<T>): Promise<IDBDocument<T>[]>,
 }
 
 export const mongoService = (db: Db) => (col: string) => {
@@ -20,7 +22,7 @@ export const mongoService = (db: Db) => (col: string) => {
     insertMany: <T>(data: T[]): Promise<any[]|IDBDocument<T>[]> => collection.insertMany(data).then(insertManyResult => insertManyResult.ops),
     findOne: <T>(query: FilterQuery<T>) => collection.findOne(query),
     findOneById: (idParam: string) => collection.findOne({ _id: new ObjectId(idParam) }),
-    find: <T>(query: FilterQuery<T>): Promise<IDBDocument<T>[]> => collection.find(query).toArray(),
+    find: <T>(query: FilterQuery<T>, optionalQuery: FilterQuery<T> = {} ): Promise<IDBDocument<T>[]> => collection.find(query, optionalQuery).toArray(),
   }
 };
 

+ 45 - 3
ktchn/src/recipes/controller.ts

@@ -1,9 +1,10 @@
 import { Request, Response, NextFunction } from 'express'
-import { Recipe } from './model';
+import { Recipe, IIngredient, IIngredientHelper } from './model';
 import { IMongoService } from '../mongo';
-import { ObjectId } from 'bson';
-import { json } from 'body-parser';
 import { FilterQuery } from 'mongodb';
+import Scrape from './scrape/index';
+import { ISubRecipe, ScrapedRecipe } from './model';
+import Ingredient from '../ingredients/model';
 
 function validRecipe(data: any): Promise<Recipe> {
   return new Promise(function(resolve, reject) {
@@ -11,6 +12,46 @@ function validRecipe(data: any): Promise<Recipe> {
   })
 }
 
+type Controller<T> = (db: IMongoService) => (req: Request, res: Response, next: NextFunction) => Promise<T>;
+
+const getPossibleValues = (db: IMongoService) => async function(ingredient: IIngredient): Promise<IIngredientHelper> {
+  const possibleValues = await db.find<Ingredient>({$text: {$search: ingredient.name}}, { score: { $meta: "textScore" } });
+  return {
+    ...ingredient,
+    possibleValues: possibleValues,
+  }
+}
+
+const scrape: Controller<void|ScrapedRecipe> = (db) => (req: any, res, next) => {
+  return Scrape(req.body.url)
+    .then(scrapedRecipe => {
+      const ingredients = scrapedRecipe.ingredients.map(subGroup => {
+        return {
+          name: subGroup.name,
+          ingredients: subGroup.ingredients.map(getPossibleValues(db))
+        }
+      });
+      return {
+        ...scrapedRecipe,
+        ingredients,
+      }
+    })
+    .then(scrapedRecipe => {
+      return Promise.all(
+        scrapedRecipe.ingredients.map(subGroup => Promise.all(subGroup.ingredients).then(ingredients => ({
+          name: subGroup.name,
+          ingredients,
+        })))
+      ).then(subgroups => {
+        res.json({
+          ...scrapedRecipe,
+          ingredients: subgroups
+        })
+      });
+    }
+    ).catch(error => console.log(error));
+}
+
 const save = (db: IMongoService) => ({ body }: Request, res: Response, next: NextFunction): Promise<any> => {
   return validRecipe(body).then(
     recipe => db.insertOne(recipe).then(
@@ -62,6 +103,7 @@ const getByIngredients = (db: IMongoService) => ({ query, params }: Request, res
 
 export {
   save,
+  scrape,
   getById,
   get,
   getAll,

+ 24 - 4
ktchn/src/recipes/model.ts

@@ -1,11 +1,12 @@
 import { ObjectID } from 'mongodb';
+import Ingredient from '../ingredients/model';
 
 interface QtyMetric {
   qty: number;
   unit: string;
 }
 
-export interface Ingredient {
+export interface IIngredient {
   name: string;
   quantity: number;
   unit: string;
@@ -14,9 +15,13 @@ export interface Ingredient {
   _original?: string;
 }
 
-export interface ComposedIngredients {
+export interface IIngredientHelper extends IIngredient { 
+  possibleValues: Ingredient[],
+}
+
+export interface ISubRecipe {
   name: string;
-  ingredients: Ingredient[];
+  ingredients: IIngredient[];
 }
 
 interface Temperature {
@@ -46,7 +51,22 @@ export interface Author {
 
 export interface Recipe {
   name: string;
-  ingredients?: ComposedIngredients[];
+  ingredients: ISubRecipe[];
+  details?: RecipeDetails;
+  instructions?: string[];
+  author?: Author;
+  website?: string;
+  tags?: string[];
+  course?: string[];
+  summary?: string;
+}
+
+export interface ScrapedRecipe {
+  name: string;
+  ingredients: {
+    name: string,
+    ingredients: IIngredientHelper[]
+  }[];
   details?: RecipeDetails;
   instructions?: string[];
   author?: Author;

+ 4 - 11
ktchn/src/recipes/routes.ts

@@ -1,7 +1,7 @@
 import { Request, Response, Router, NextFunction } from "express";
 import Scrape from "./scrape";
 import { Recipe } from './model';
-import { save, get, getById, getAll, getByIngredients } from './controller';
+import { save, get, getById, getAll, getByIngredients, scrape } from './controller';
 import MongoClient from 'mongodb';
 import { mongoService, IMongoService } from '../mongo';
 import piddleware from '../promise-all-middleware';
@@ -9,10 +9,12 @@ import piddleware from '../promise-all-middleware';
 class RecipeRoutes {
   public router: Router;
   private RecipeDB: IMongoService;
+  private IngredientDB: IMongoService;
 
   public constructor(db: MongoClient.Db) {
     this.router = Router();
     this.RecipeDB = mongoService(db)('recipes');
+    this.IngredientDB = mongoService(db)('ingredients');
     this.init();
   }
 
@@ -23,16 +25,7 @@ class RecipeRoutes {
     this.router.get("/id/:id", piddleware([getById(this.RecipeDB)]));
     
     this.router.post("/", piddleware([save(this.RecipeDB)]));
-    this.router.post("/scrape", (req, res, next) => {
-       return Scrape(req.body.url).then((x)=>{
-         res.json(x)
-       }).catch((error: Error) => res.status(400).send(
-        {
-          name: error.name,
-          message: error.message,
-        }
-      ));
-    })
+    this.router.post("/scrape", piddleware([scrape(this.IngredientDB)]));
   }
 
   private logData(req:Request, res:Response, next: NextFunction): void {

+ 2 - 2
ktchn/src/recipes/scrape/index.ts

@@ -2,7 +2,7 @@ import RequestPromise from "request-promise";
 import Cheerio from "cheerio";
 import Sources from './sources';
 import { ScrapingSource } from './sources/index';
-import { Ingredient } from '../model';
+import { IIngredient } from '../model';
 import { parse as parseIngredient } from 'recipe-ingredient-parser';
 
 function selectSourceAsync(url: string): Promise<ScrapingSource> {
@@ -18,7 +18,7 @@ function selectSourceAsync(url: string): Promise<ScrapingSource> {
   });
 }
 
-export const parseIngredients = (rawIngredient: string): Ingredient => {
+export const parseIngredients = (rawIngredient: string): IIngredient => {
   let parsed = parseIngredient(rawIngredient);
   return {
     name: parsed.ingredient,

+ 3 - 3
ktchn/src/recipes/scrape/sources/all-recipes.ts

@@ -1,4 +1,4 @@
-import { Recipe, Author, RecipeDetails, ComposedIngredients } from '../../model';
+import { Recipe, Author, RecipeDetails, ISubRecipe } from '../../model';
 import { parseIngredients } from '../index';
 import { getText, getAttr, getTextList } from './../service';
 
@@ -24,10 +24,10 @@ function getRecipeDetails($: CheerioSelector): RecipeDetails {
   };
 }
 
-function getIngredients($: CheerioSelector): ComposedIngredients[] {
+function getIngredients($: CheerioSelector): ISubRecipe[] {
   const rawData = $(SELECTORS.INGREDIENTS).find('li.checkList__line');
   const isSubtitle = (element: Cheerio) => element.find('input').data('id') === 0;
-  let ingredients: ComposedIngredients[] = [{
+  let ingredients: ISubRecipe[] = [{
     name: '',
     ingredients: [],
   }];

+ 3 - 3
ktchn/src/recipes/scrape/sources/home-cooking-adventure.ts

@@ -1,5 +1,5 @@
 import { Recipe } from "../../model";
-import { RecipeDetails, ComposedIngredients } from '../../model';
+import { RecipeDetails, ISubRecipe } from '../../model';
 import { parseIngredients } from '../index';
 
 const SELECTORS = {
@@ -24,9 +24,9 @@ function getRecipeDetails($: CheerioSelector): RecipeDetails {
   }
 }
 
-function getIngredients($: CheerioSelector): ComposedIngredients[] {
+function getIngredients($: CheerioSelector): ISubRecipe[] {
   const rawData = $(SELECTORS.INGREDIENTS);
-  let ingredients: ComposedIngredients[] = [{name: '', ingredients: []}];
+  let ingredients: ISubRecipe[] = [{name: '', ingredients: []}];
   const isSubtitle = (element: Cheerio) => element.find('span').hasClass('ingheading');
   const removeEquivalence = (str: string): string => str.replace(/\([0-9.]+\w+\) ?/, '');
 

+ 3 - 3
ktchn/src/recipes/scrape/sources/joy-of-baking.ts

@@ -1,4 +1,4 @@
-import { Recipe, Author, RecipeDetails, ComposedIngredients } from '../../model';
+import { Recipe, Author, RecipeDetails, ISubRecipe } from '../../model';
 import { parseIngredients } from '../index';
 import { rmBreakLines, getText, rmEquivalence } from './../service';
 
@@ -15,10 +15,10 @@ function getRecipeName($: CheerioSelector): string {
   return getText($)(SELECTORS.TITLE).replace(/(.\n)?.Recipe.*/, '').trim();
 }
 
-function getIngredients($: CheerioSelector): ComposedIngredients[] {
+function getIngredients($: CheerioSelector): ISubRecipe[] {
   const rawData = $('td [width="252"] p').toArray();
   const isIngredient = (element: CheerioElement): boolean => $(element).hasClass('ingredient');
-  let ingredients: ComposedIngredients[] = [{name: '', ingredients: []}];
+  let ingredients: ISubRecipe[] = [{name: '', ingredients: []}];
   return rawData.reduce((list:any, element, index) => {
     let rawIngredient = $(element).text();
     rawIngredient = rmBreakLines(rawIngredient);

+ 5 - 5
ktchn/src/recipes/scrape/sources/just-one-cookbook.ts

@@ -1,5 +1,5 @@
-import { Recipe, Ingredient } from "../../model";
-import { RecipeDetails, ComposedIngredients } from '../../model';
+import { Recipe, IIngredient } from "../../model";
+import { RecipeDetails, ISubRecipe } from '../../model';
 import { parseIngredients } from '../index';
 import { getText, getTextList, rmEquivalence } from './../service';
 import R from 'ramda';
@@ -36,13 +36,13 @@ const getRecipeDetails = ($: CheerioSelector): RecipeDetails => {
   }
 };
 
-const getIngredients = ($: CheerioSelector): ComposedIngredients[] => {
-  let ingredients: ComposedIngredients[] = [{name: '', ingredients: []}];
+const getIngredients = ($: CheerioSelector): ISubRecipe[] => {
+  let ingredients: ISubRecipe[] = [{name: '', ingredients: []}];
   const rawData = $(SELECTORS.INGREDIENTS);
   const cleanName = (element: CheerioElement, not: string): string => $(element).children().not(not).toArray().map(x => $(x).text()).join(' ');
   rawData.each((i, element) => {
     const last = ingredients.length - 1;
-    const getIngredients = (li: Cheerio): Ingredient[] => li.toArray().map(el => ({
+    const getIngredients = (li: Cheerio): IIngredient[] => li.toArray().map(el => ({
       ...parseIngredients(cleanName(el, SELECTORS.ING.NOTE)),
       note: $(el).find(SELECTORS.ING.NOTE).text(),
       _original: $(el).text().trim(),

+ 4 - 4
ktchn/src/recipes/scrape/sources/laura-in-the-kitchen.ts

@@ -1,4 +1,4 @@
-import { Recipe, RecipeDetails, Ingredient, ComposedIngredients, Author } from '../../model';
+import { Recipe, RecipeDetails, IIngredient, ISubRecipe, Author } from '../../model';
 import { parse as parseIngredient } from "recipe-ingredient-parser";
 import { getText, getTextList, getAttr } from "./../service";
 
@@ -25,8 +25,8 @@ function getRecipeDetails($: CheerioSelector): RecipeDetails {
   );
 }
 
-function getIngredients($: CheerioSelector): ComposedIngredients[] {
-  const parseIngredients = (rawIngredient: string): Ingredient => {
+function getIngredients($: CheerioSelector): ISubRecipe[] {
+  const parseIngredients = (rawIngredient: string): IIngredient => {
     let parsed = parseIngredient(rawIngredient);
     return {
       name: parsed.ingredient,
@@ -38,7 +38,7 @@ function getIngredients($: CheerioSelector): ComposedIngredients[] {
   const ingredientsScrape = $(SELECTORS.INGREDIENTS).children();
   const isNewIngredientList = (tagName: string): boolean => tagName === 'span';
   
-  let ingredients: ComposedIngredients[] = [{name: '', ingredients: []}];
+  let ingredients: ISubRecipe[] = [{name: '', ingredients: []}];
   return ingredientsScrape.toArray().reduce((list, rawIngredient)=>{
     let last = list.length - 1;
     let text = $(rawIngredient).text();