diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..c0e01ca --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/package.json b/package.json index f70ca99..186e93a 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "cmdk": "^1.0.0", - "drizzle-orm": "^0.30.6", + "drizzle-orm": "^0.30.9", "jsdom": "^23.0.1", "lucide-react": "^0.368.0", "next": "^14.0.3", @@ -75,7 +75,7 @@ "@typescript-eslint/parser": "^7.4.0", "autoprefixer": "^10.4.14", "dotenv-cli": "^7.3.0", - "drizzle-kit": "^0.20.14", + "drizzle-kit": "^0.20.17", "eslint": "^8.57.0", "eslint-config-next": "13.0.0", "eslint-config-prettier": "^8.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8a5674..d56cc0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -72,8 +72,8 @@ dependencies: specifier: ^1.0.0 version: 1.0.0(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) drizzle-orm: - specifier: ^0.30.6 - version: 0.30.6(@planetscale/database@1.11.0)(@types/better-sqlite3@7.6.8)(@types/react@18.2.37)(better-sqlite3@9.2.2)(mysql2@3.9.7)(react@18.2.0) + specifier: ^0.30.9 + version: 0.30.9(@planetscale/database@1.11.0)(@types/better-sqlite3@7.6.8)(@types/react@18.2.37)(better-sqlite3@9.2.2)(mysql2@3.9.7)(react@18.2.0) jsdom: specifier: ^23.0.1 version: 23.0.1 @@ -167,8 +167,8 @@ devDependencies: specifier: ^7.3.0 version: 7.3.0 drizzle-kit: - specifier: ^0.20.14 - version: 0.20.14 + specifier: ^0.20.17 + version: 0.20.17 eslint: specifier: ^8.57.0 version: 8.57.0 @@ -404,12 +404,6 @@ packages: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 - /@drizzle-team/studio@0.0.39: - resolution: {integrity: sha512-c5Hkm7MmQC2n5qAsKShjQrHoqlfGslB8+qWzsGGZ+2dHMRTNG60UuzalF0h0rvBax5uzPXuGkYLGaQ+TUX3yMw==} - dependencies: - superjson: 2.2.1 - dev: true - /@ericcornelissen/bash-parser@0.5.2: resolution: {integrity: sha512-4pIMTa1nEFfMXitv7oaNEWOdM+zpOZavesa5GaiWTgda6Zk32CFGxjUp/iIaN0PwgUW1yTq/fztSjbpE8SLGZQ==} engines: {node: '>=4'} @@ -896,6 +890,21 @@ packages: resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==} dev: false + /@hono/node-server@1.11.0: + resolution: {integrity: sha512-TLIJq9TMtD1NEG1mVoqNUn1Ita0qSaB5XboZErjFBcO/GJYXwWY4dVdTi9G0lbxtu0x+hJXDItcLaFHb7rlFTw==} + engines: {node: '>=18.14.1'} + dev: true + + /@hono/zod-validator@0.2.1(hono@4.2.7)(zod@3.22.4): + resolution: {integrity: sha512-HFoxln7Q6JsE64qz2WBS28SD33UB2alp3aRKmcWnNLDzEL1BLsWfbdX6e1HIiUprHYTIXf5y7ax8eYidKUwyaA==} + peerDependencies: + hono: '>=3.9.0' + zod: ^3.19.1 + dependencies: + hono: 4.2.7 + zod: 3.22.4 + dev: true + /@hookform/resolvers@3.3.3(react-hook-form@7.49.2): resolution: {integrity: sha512-bOMxKkSD3zWcS11TKoUQ8O0ZqKslFohvUsPKSrdCHiuEuMjRo/u3cq9YRJD/+xtNGYup++XD2LkjhegP5XENiw==} peerDependencies: @@ -3246,12 +3255,13 @@ packages: wordwrap: 1.0.0 dev: true - /drizzle-kit@0.20.14: - resolution: {integrity: sha512-0fHv3YIEaUcSVPSGyaaBfOi9bmpajjhbJNdPsRMIUvYdLVxBu9eGjH8mRc3Qk7HVmEidFc/lhG1YyJhoXrn5yA==} + /drizzle-kit@0.20.17: + resolution: {integrity: sha512-mLVDS4nXmO09wFVlzGrdshWnAL+U9eQGC5zRs6hTN6Q9arwQGWU2XnZ17I8BM8Quau8CQRx3Ms6VPgRWJFVp7Q==} hasBin: true dependencies: - '@drizzle-team/studio': 0.0.39 '@esbuild-kit/esm-loader': 2.6.5 + '@hono/node-server': 1.11.0 + '@hono/zod-validator': 0.2.1(hono@4.2.7)(zod@3.22.4) camelcase: 7.0.1 chalk: 5.3.0 commander: 9.5.0 @@ -3260,16 +3270,18 @@ packages: esbuild-register: 3.5.0(esbuild@0.19.12) glob: 8.1.0 hanji: 0.0.5 + hono: 4.2.7 json-diff: 0.9.0 minimatch: 7.4.6 semver: 7.6.0 + superjson: 2.2.1 zod: 3.22.4 transitivePeerDependencies: - supports-color dev: true - /drizzle-orm@0.30.6(@planetscale/database@1.11.0)(@types/better-sqlite3@7.6.8)(@types/react@18.2.37)(better-sqlite3@9.2.2)(mysql2@3.9.7)(react@18.2.0): - resolution: {integrity: sha512-8RgNUmY7J03GRuRgBV5SaJNbYgLVPjdSWNS/bRkIMIHt2TFCA439lJsNpqYX8asyKMqkw8ceBiamUnCIXZIt9w==} + /drizzle-orm@0.30.9(@planetscale/database@1.11.0)(@types/better-sqlite3@7.6.8)(@types/react@18.2.37)(better-sqlite3@9.2.2)(mysql2@3.9.7)(react@18.2.0): + resolution: {integrity: sha512-VOiCFsexErmgqvNCOmbzmqDCZzZsHoz6SkWAjTFxsTr1AllKDbDJ2+GgedLXsXMDgpg/ljDG1zItIFeZtiO2LA==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' '@cloudflare/workers-types': '>=3' @@ -3283,7 +3295,7 @@ packages: '@types/pg': '*' '@types/react': '>=18' '@types/sql.js': '*' - '@vercel/postgres': '*' + '@vercel/postgres': '>=0.8.0' '@xata.io/client': '*' better-sqlite3: '>=7' bun-types: '*' @@ -4486,6 +4498,11 @@ packages: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} dev: true + /hono@4.2.7: + resolution: {integrity: sha512-k1xHi86tJnRIVvqhFMBDGFKJ8r5O+bEsT4P59ZK59r0F300Xd910/r237inVfuT/VmE86RQQffX4OYNda6dLXw==} + engines: {node: '>=16.0.0'} + dev: true + /hosted-git-info@4.1.0: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} diff --git a/src/app/(dashboard)/recipes/[id]/recipeForm.tsx b/src/app/(dashboard)/recipes/[id]/recipeForm.tsx index ef1db0b..6b0dd96 100644 --- a/src/app/(dashboard)/recipes/[id]/recipeForm.tsx +++ b/src/app/(dashboard)/recipes/[id]/recipeForm.tsx @@ -13,6 +13,14 @@ import { Controller, FormProvider, useForm } from "react-hook-form" import { toast } from "sonner" import { Button } from "~/components/ui/button" +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "~/components/ui/form" +import { Input } from "~/components/ui/input" import { IngredientTable } from "~/components/ingredient-table" import { RecipeTitleInput } from "~/components/recipe-title-input" @@ -52,9 +60,10 @@ function RecipeFormInner({ } }), recipeName: recipe.name, - method: recipe.steps || undefined, - imageUrl: recipe.imageUrl || undefined, + method: recipe.steps ?? undefined, + imageUrl: recipe.imageUrl ?? undefined, recipeBuddyRecipeId: recipe.id, + servings: recipe.servings ?? undefined, }, }) @@ -81,6 +90,19 @@ function RecipeFormInner({ name="recipeName" control={form.control} /> + ( + + Servings + + + + + + )} + name="servings" + control={form.control} + /> { + if (a < 1) return 1 + return a + }) + export const CreateRecipeInGrocyCommandSchema = z.object({ recipeBuddyRecipeId: z.number(), recipeName: z.string().trim().min(1), ingredients: IngredientSchema.array(), method: z.string().optional(), imageUrl: z.string().url().optional(), + servings: numberLikeToNumberAtLeastOne.optional(), }) export type CreateRecipeInGrocyCommand = z.infer< diff --git a/src/server/api/modules/recipes/service/schemas.ts b/src/server/api/modules/recipes/service/schemas.ts index b47707f..91bfcd8 100644 --- a/src/server/api/modules/recipes/service/schemas.ts +++ b/src/server/api/modules/recipes/service/schemas.ts @@ -18,5 +18,23 @@ export const RecipeImageUrlSchema = z }) export const JsonLdRecipeSchema = z.object({ - "@type": z.string(), + "@type": z.union([z.string(), z.tuple([z.string()]).transform((a) => a[0])]), +}) + +export const ExtractNumberSchema = z.coerce.string().transform((val, ctx) => { + const numberRegex = /\d+/g + + const regexResult = numberRegex.exec(val) + + if (!regexResult) { + ctx.addIssue({ + message: "No numbers found in servings", + code: "custom", + }) + return z.NEVER + } + + const [first] = regexResult + + return parseInt(first) }) diff --git a/src/server/api/modules/recipes/service/scraper.ts b/src/server/api/modules/recipes/service/scraper.ts index 1503e85..c95f1b6 100644 --- a/src/server/api/modules/recipes/service/scraper.ts +++ b/src/server/api/modules/recipes/service/scraper.ts @@ -1,5 +1,6 @@ import { TRPCError } from "@trpc/server" import { + ExtractNumberSchema, JsonLdRecipeSchema, RecipeImageUrlSchema, RecipeStepSchema, @@ -62,6 +63,7 @@ function getSchemaRecipeFromNodeList(nodeList: NodeList) { } if (Array.isArray(parsedNodeContent)) { + console.log("its an array") for (const metadataObject of parsedNodeContent) { if (jsonObjectIsRecipe(metadataObject)) { return metadataObject @@ -106,11 +108,14 @@ export async function hydrateRecipe(url: string) { (a) => ({ scrapedName: a }) ) + const servings = ExtractNumberSchema.safeParse(recipeData.recipeYield) + const recipe: InsertRecipe = { name: recipeData.name, url, steps: steps.data.join("\n"), imageUrl: image.success ? image.data : undefined, + servings: servings.success ? servings.data : undefined, } return { recipe, ingredients: ings } diff --git a/src/server/db/drizzle/0001_worthless_aqueduct.sql b/src/server/db/drizzle/0001_worthless_aqueduct.sql new file mode 100644 index 0000000..5ec8e6b --- /dev/null +++ b/src/server/db/drizzle/0001_worthless_aqueduct.sql @@ -0,0 +1 @@ +ALTER TABLE `recipe-buddy_recipe` ADD `servings` integer; diff --git a/src/server/db/drizzle/meta/0001_snapshot.json b/src/server/db/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..001cfb6 --- /dev/null +++ b/src/server/db/drizzle/meta/0001_snapshot.json @@ -0,0 +1,153 @@ +{ + "version": "5", + "dialect": "sqlite", + "id": "ce10c78c-b332-4765-b5d1-932f1db6cf33", + "prevId": "6d9f7961-4666-469a-bc84-92afae7f2c60", + "tables": { + "recipe-buddy_ingredient": { + "name": "recipe-buddy_ingredient", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "scrapedName": { + "name": "scrapedName", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipeId": { + "name": "recipeId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "recipe-buddy_ingredient_recipeId_recipe-buddy_recipe_id_fk": { + "name": "recipe-buddy_ingredient_recipeId_recipe-buddy_recipe_id_fk", + "tableFrom": "recipe-buddy_ingredient", + "tableTo": "recipe-buddy_recipe", + "columnsFrom": ["recipeId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "recipe-buddy_recipe": { + "name": "recipe-buddy_recipe", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "steps": { + "name": "steps", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "imageUrl": { + "name": "imageUrl", + "type": "text(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "servings": { + "name": "servings", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "recipe-buddy_user": { + "name": "recipe-buddy_user", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "passwordHash": { + "name": "passwordHash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "recipe-buddy_user_username_unique": { + "name": "recipe-buddy_user_username_unique", + "columns": ["username"], + "isUnique": true + }, + "username_idx": { + "name": "username_idx", + "columns": ["name"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} diff --git a/src/server/db/drizzle/meta/_journal.json b/src/server/db/drizzle/meta/_journal.json index be297f9..4bd91b6 100644 --- a/src/server/db/drizzle/meta/_journal.json +++ b/src/server/db/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1711809186824, "tag": "0000_rare_sauron", "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1713943610110, + "tag": "0001_worthless_aqueduct", + "breakpoints": true } ] } diff --git a/src/server/db/migrate.ts b/src/server/db/migrate.ts index 020152f..4969304 100644 --- a/src/server/db/migrate.ts +++ b/src/server/db/migrate.ts @@ -1,7 +1,14 @@ +import { dirname, join } from "path" +import { fileURLToPath } from "url" import { migrate } from "drizzle-orm/better-sqlite3/migrator" import { db, sqlite } from "./index" -await migrate(db, { migrationsFolder: "./drizzle" }) +const __filename = fileURLToPath(import.meta.url) // get the resolved path to the file +const __dirname = dirname(__filename) + +const migrationsPath = join(__dirname, "drizzle") + +await migrate(db, { migrationsFolder: migrationsPath }) await sqlite.close() diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 40515e6..96b5c3d 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -14,6 +14,7 @@ export const recipes = sqLiteTable("recipe", { url: text("url", { length: 512 }).notNull(), steps: text("steps"), imageUrl: text("imageUrl", { length: 256 }), + servings: integer("servings", { mode: "number" }), }) export const recipeRelations = relations(recipes, ({ many }) => ({