Skip to content

Commit 6b21009

Browse files
committed
feat: update NutritionData model to include unique code and add indexing
1 parent ae85d18 commit 6b21009

File tree

7 files changed

+53
-33
lines changed

7 files changed

+53
-33
lines changed

apps/svelte/prisma/schema/macros.prisma

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ model FoodEntry {
4747
}
4848

4949
model NutritionData {
50-
id Int @id @default(autoincrement())
51-
code String
52-
product_name String
53-
brands String?
54-
quantity String?
55-
foodEntries FoodEntry[]
50+
id Int @id @default(autoincrement())
51+
code String @unique
52+
product_name String
53+
brands String?
54+
search_vector Unsupported("tsvector") @default(dbgenerated())
55+
foodEntries FoodEntry[]
5656
5757
energy_kcal_100g Float
5858
proteins_100g Float

apps/svelte/src/lib/prisma-idb/idb-interface.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ export interface PrismaIDBSchema extends DBSchema {
7575
NutritionData: {
7676
key: [id: Prisma.NutritionData['id']];
7777
value: Prisma.NutritionData;
78+
indexes: {
79+
codeIndex: [code: Prisma.NutritionData['code']];
80+
};
7881
};
7982
GettingStartedAnswers: {
8083
key: [id: Prisma.GettingStartedAnswers['id']];

apps/svelte/src/lib/prisma-idb/prisma-idb-client.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ export class PrismaIDBClient {
9595
unique: true
9696
});
9797
db.createObjectStore('FoodEntry', { keyPath: ['id'] });
98-
db.createObjectStore('NutritionData', { keyPath: ['id'] });
98+
const NutritionDataStore = db.createObjectStore('NutritionData', { keyPath: ['id'] });
99+
NutritionDataStore.createIndex('codeIndex', ['code'], { unique: true });
99100
const GettingStartedAnswersStore = db.createObjectStore('GettingStartedAnswers', {
100101
keyPath: ['id']
101102
});
@@ -10801,7 +10802,7 @@ class FoodEntryIDBClass extends BaseIDBModelClass<'FoodEntry'> {
1080110802
const deleteWhere =
1080210803
query.data.nutritionData.delete === true ? {} : query.data.nutritionData.delete;
1080310804
await this.client.nutritionData.delete(
10804-
{ where: { ...deleteWhere, id: record.nutritionDataId! } },
10805+
{ where: { ...deleteWhere, id: record.nutritionDataId! } /* TODO: add this > as Prisma.NutritionDataWhereUniqueInput */ },
1080510806
tx
1080610807
);
1080710808
record.nutritionDataId = null;
@@ -11055,7 +11056,7 @@ class NutritionDataIDBClass extends BaseIDBModelClass<'NutritionData'> {
1105511056
return (
1105611057
await Promise.all(
1105711058
records.map(async (record) => {
11058-
const stringFields = ['code', 'product_name', 'brands', 'quantity'] as const;
11059+
const stringFields = ['code', 'product_name', 'brands'] as const;
1105911060
for (const field of stringFields) {
1106011061
if (!IDBUtils.whereStringFilter(record, field, whereClause[field])) return null;
1106111062
}
@@ -11150,7 +11151,6 @@ class NutritionDataIDBClass extends BaseIDBModelClass<'NutritionData'> {
1115011151
'code',
1115111152
'product_name',
1115211153
'brands',
11153-
'quantity',
1115411154
'foodEntries',
1115511155
'energy_kcal_100g',
1115611156
'proteins_100g',
@@ -11265,7 +11265,6 @@ class NutritionDataIDBClass extends BaseIDBModelClass<'NutritionData'> {
1126511265
'code',
1126611266
'product_name',
1126711267
'brands',
11268-
'quantity',
1126911268
'energy_kcal_100g',
1127011269
'proteins_100g',
1127111270
'fat_100g',
@@ -11317,7 +11316,6 @@ class NutritionDataIDBClass extends BaseIDBModelClass<'NutritionData'> {
1131711316
'code',
1131811317
'product_name',
1131911318
'brands',
11320-
'quantity',
1132111319
'energy_kcal_100g',
1132211320
'proteins_100g',
1132311321
'fat_100g',
@@ -11375,9 +11373,6 @@ class NutritionDataIDBClass extends BaseIDBModelClass<'NutritionData'> {
1137511373
if (data.brands === undefined) {
1137611374
data.brands = null;
1137711375
}
11378-
if (data.quantity === undefined) {
11379-
data.quantity = null;
11380-
}
1138111376
if (data.saturated_fat_100g === undefined) {
1138211377
data.saturated_fat_100g = null;
1138311378
}
@@ -11707,6 +11702,8 @@ class NutritionDataIDBClass extends BaseIDBModelClass<'NutritionData'> {
1170711702
let record;
1170811703
if (query.where.id !== undefined) {
1170911704
record = await tx.objectStore('NutritionData').get([query.where.id]);
11705+
} else if (query.where.code !== undefined) {
11706+
record = await tx.objectStore('NutritionData').index('codeIndex').get([query.where.code]);
1171011707
}
1171111708
if (!record) return null;
1171211709

@@ -11917,7 +11914,7 @@ class NutritionDataIDBClass extends BaseIDBModelClass<'NutritionData'> {
1191711914
throw new Error('Record not found');
1191811915
}
1191911916
const startKeyPath: PrismaIDBSchema['NutritionData']['key'] = [record.id];
11920-
const stringFields = ['code', 'product_name', 'brands', 'quantity'] as const;
11917+
const stringFields = ['code', 'product_name', 'brands'] as const;
1192111918
for (const field of stringFields) {
1192211919
IDBUtils.handleStringUpdateField(record, field, query.data[field]);
1192311920
}
@@ -12229,7 +12226,7 @@ class NutritionDataIDBClass extends BaseIDBModelClass<'NutritionData'> {
1222912226
.filter((value) => value !== undefined);
1223012227
(minResult[field as keyof typeof minResult] as number) = Math.min(...values);
1223112228
}
12232-
const stringFields = ['code', 'product_name', 'brands', 'quantity'] as const;
12229+
const stringFields = ['code', 'product_name', 'brands'] as const;
1223312230
for (const field of stringFields) {
1223412231
if (!query._min[field]) continue;
1223512232
const values = records
@@ -12287,7 +12284,7 @@ class NutritionDataIDBClass extends BaseIDBModelClass<'NutritionData'> {
1228712284
.filter((value) => value !== undefined);
1228812285
(maxResult[field as keyof typeof maxResult] as number) = Math.max(...values);
1228912286
}
12290-
const stringFields = ['code', 'product_name', 'brands', 'quantity'] as const;
12287+
const stringFields = ['code', 'product_name', 'brands'] as const;
1229112288
for (const field of stringFields) {
1229212289
if (!query._max[field]) continue;
1229312290
const values = records

apps/svelte/src/routes/(app)/food-diary/add/manual/+page.server.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export async function load() {
66
code: '2',
77
product_name: 'Filets de poulet blanc x2',
88
brands: 'SoLo',
9-
quantity: '240-400 g',
109
energy_kcal_100g: 141,
1110
proteins_100g: 30,
1211
fat_100g: 2.7,

apps/svelte/src/routes/(app)/food-diary/add/manual/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
const nutrimentLabels = getNutrimentLabels(data.sampleNutritionData);
1717
1818
function getNutrimentLabels(sampleData: NutritionData) {
19-
const { code, quantity, product_name, brands, ...nutriments } = sampleData;
19+
const { code, product_name, brands, ...nutriments } = sampleData;
2020
const { energy_kcal_100g, proteins_100g, carbohydrates_100g, fat_100g, ...others } = nutriments;
2121
2222
return Object.keys(others).map((key) => capitalizeWords(snakeToNormal(key).slice(0, -4)));

apps/svelte/src/routes/(app)/food-diary/add/scan/+page.svelte

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,31 @@
33
import H2 from '$lib/components/typography/h2.svelte';
44
import H3 from '$lib/components/typography/h3.svelte';
55
import Button from '$lib/components/ui/button/button.svelte';
6-
import type { NutritionData } from '@prisma/client';
76
import * as Card from '$lib/components/ui/card/index.js';
87
import { createQuery } from '@tanstack/svelte-query';
98
import { BarqodeStream, type DetectedBarcode } from 'barqode';
10-
import { LoaderCircleIcon, PencilIcon, RedoIcon, ScanTextIcon, SearchIcon } from 'lucide-svelte';
9+
import {
10+
LoaderCircleIcon,
11+
PencilIcon,
12+
PlusIcon,
13+
RedoIcon,
14+
ScanTextIcon,
15+
SearchIcon
16+
} from 'lucide-svelte';
1117
import { toast } from 'svelte-sonner';
18+
import { getFoodByCode } from '../../food.remote';
1219
1320
let loading = $state(true);
1421
let paused = $state(false);
15-
let result = $state<number>();
22+
let result = $state<string>();
1623
1724
function onCameraOn() {
1825
loading = false;
1926
}
2027
2128
function onDetect(detectedCodes: DetectedBarcode[]) {
22-
const value = Number(detectedCodes.at(-1)?.rawValue);
23-
if (isNaN(value)) {
29+
const value = detectedCodes.at(-1)?.rawValue;
30+
if (typeof value !== 'string') {
2431
toast.error('Detected code is not a valid number');
2532
return;
2633
}
@@ -33,13 +40,11 @@
3340
queryKey: ['barcode', result],
3441
queryFn: async () => {
3542
try {
36-
const response = await fetch(`/api/food/${result}`);
37-
if (response.ok) return (await response.json()) as NutritionData;
38-
else if (response.status === 404) return null;
39-
else throw new Error('Error occurred while fetching food data');
43+
return await getFoodByCode({ code: result! });
4044
} catch (error) {
4145
toast.error('Failed to fetch food data');
4246
console.error('Error fetching food data:', error);
47+
return null;
4348
}
4449
},
4550
enabled: Boolean(result)
@@ -81,13 +86,23 @@
8186
<span>Error fetching food data</span>
8287
</div>
8388
{:else if barcodeQuery.data}
84-
<div class="flex flex-col items-center gap-2">
85-
<span class="text-lg font-semibold">Food found:</span>
86-
<span class="text-xl">{barcodeQuery.data.product_name}</span>
89+
<div class="flex flex-col">
90+
<span class="text-lg">{barcodeQuery.data.product_name}</span>
8791
<span class="text-muted-foreground text-sm">
8892
{barcodeQuery.data.brands}
8993
</span>
9094
</div>
95+
<Button class="mt-4 w-full"><PlusIcon /> Add to diary</Button>
96+
<Button
97+
class="mt-2 w-full"
98+
variant="outline"
99+
onclick={() => {
100+
result = undefined;
101+
paused = false;
102+
}}
103+
>
104+
<RedoIcon /> Try again
105+
</Button>
91106
{:else}
92107
<div class="grid gap-2">
93108
<span>No food data found for this barcode</span>

apps/svelte/src/routes/(app)/food-diary/food.remote.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@ function buildPrefixQuery(search: string): string {
1111
.join(' & ');
1212
}
1313

14-
export const getFoodById = command(z.strictObject({ id: z.int() }), async (data) => {
14+
export const getFoodById = command(z.strictObject({ id: z.number() }), async (data) => {
1515
return await prisma.nutritionData.findUniqueOrThrow({
1616
where: { id: data.id }
1717
});
1818
});
1919

20+
export const getFoodByCode = command(z.strictObject({ code: z.string() }), async (data) => {
21+
return await prisma.nutritionData.findUniqueOrThrow({
22+
where: { code: data.code }
23+
});
24+
});
25+
2026
export const searchFoods = query(z.strictObject({ query: z.string() }), async (data) => {
2127
const results = await prisma.$queryRawTyped(searchFoodsQuery(buildPrefixQuery(data.query)));
2228

0 commit comments

Comments
 (0)