Multi tenant DB, registratie per huishouden V1.2

This commit is contained in:
2026-08-09 19:50:53 +02:00
parent 1f9fd73026
commit c37b8956d4
15 changed files with 148 additions and 96 deletions
+6 -6
View File
@@ -8,7 +8,7 @@ class CategoryController {
res.render("categories/index", { res.render("categories/index", {
title: "Categorieën", title: "Categorieën",
categories: CategoryService.getCategories() categories: CategoryService.getCategories(req.user.household_id)
}); });
} }
@@ -17,7 +17,7 @@ class CategoryController {
editForm(req, res) { editForm(req, res) {
const categoryId = Number.parseInt(req.params.id, 10); const categoryId = Number.parseInt(req.params.id, 10);
const category = Number.isInteger(categoryId) const category = Number.isInteger(categoryId)
? CategoryService.getCategory(categoryId) ? CategoryService.getCategory(categoryId, req.user.household_id)
: null; : null;
if (!category) { if (!category) {
@@ -41,7 +41,7 @@ class CategoryController {
return res.redirect("/categories"); return res.redirect("/categories");
} }
CategoryService.createCategory({ name, icon }); CategoryService.createCategory({ name, icon, household_id: req.user.household_id });
req.flash("success", "Categorie toegevoegd."); req.flash("success", "Categorie toegevoegd.");
res.redirect("/categories"); res.redirect("/categories");
} }
@@ -57,7 +57,7 @@ class CategoryController {
return res.redirect("/categories"); return res.redirect("/categories");
} }
const result = CategoryService.updateCategory(categoryId, { name, icon }); const result = CategoryService.updateCategory(categoryId, req.user.household_id, { name, icon });
if (result.changes === 0) { if (result.changes === 0) {
req.flash("error", "Categorie niet gevonden."); req.flash("error", "Categorie niet gevonden.");
@@ -77,7 +77,7 @@ class CategoryController {
return res.redirect("/categories"); return res.redirect("/categories");
} }
if (CategoryService.categoryHasProducts(categoryId)) { if (CategoryService.categoryHasProducts(categoryId, req.user.household_id)) {
req.flash( req.flash(
"error", "error",
"Deze categorie is nog aan producten gekoppeld en kan niet worden verwijderd." "Deze categorie is nog aan producten gekoppeld en kan niet worden verwijderd."
@@ -85,7 +85,7 @@ class CategoryController {
return res.redirect("/categories"); return res.redirect("/categories");
} }
const result = CategoryService.deleteCategory(categoryId); const result = CategoryService.deleteCategory(categoryId, req.user.household_id);
if (result.changes === 0) { if (result.changes === 0) {
req.flash("error", "Categorie niet gevonden."); req.flash("error", "Categorie niet gevonden.");
+1 -1
View File
@@ -25,7 +25,7 @@ class MenuController {
res.render("menu/preview", { res.render("menu/preview", {
title: "Weekmenu controleren", title: "Weekmenu controleren",
menu, menu,
newProducts: MenuService.getNewProducts(menu), newProducts: MenuService.getNewProducts(menu, req.user.household_id),
listName: "Weekboodschappen" listName: "Weekboodschappen"
}); });
} catch (error) { } catch (error) {
+24 -15
View File
@@ -1,7 +1,8 @@
const ProductService = const ProductService =
require("../services/ProductService"); require("../services/ProductService");
const db = require("../config/database"); const CategoryService = require("../services/CategoryService");
const parseCsv = require("../utils/parseCsv"); const parseCsv = require("../utils/parseCsv");
class ProductController { class ProductController {
@@ -10,11 +11,9 @@ class ProductController {
index(req, res) { index(req, res) {
const products = const products =
ProductService.getProducts(); ProductService.getProducts(req.user.household_id);
const categories = db.prepare( const categories = CategoryService.getCategories(req.user.household_id);
"SELECT * FROM categories ORDER BY name"
).all();
res.render( res.render(
"products/index", "products/index",
@@ -39,9 +38,15 @@ class ProductController {
return res.redirect("/products"); return res.redirect("/products");
} }
if (categoryId !== null && !CategoryService.getCategory(categoryId, req.user.household_id)) {
req.flash("error", "De gekozen categorie bestaat niet.");
return res.redirect("/products");
}
ProductService.createProduct({ ProductService.createProduct({
name, name,
category_id: categoryId category_id: categoryId,
household_id: req.user.household_id
}); });
req.flash("success", "Product toegevoegd."); req.flash("success", "Product toegevoegd.");
@@ -80,7 +85,7 @@ class ProductController {
return res.redirect("/products"); return res.redirect("/products");
} }
const categories = db.prepare("SELECT id, name FROM categories").all(); const categories = CategoryService.getCategories(req.user.household_id);
const categoryIds = new Map( const categoryIds = new Map(
categories.map(category => [category.name.trim().toLowerCase(), category.id]) categories.map(category => [category.name.trim().toLowerCase(), category.id])
); );
@@ -105,7 +110,8 @@ class ProductController {
products.push({ products.push({
name, name,
category_id: categoryId, category_id: categoryId,
barcode: barcodeIndex === -1 ? "" : (row[barcodeIndex] || "").trim() barcode: barcodeIndex === -1 ? "" : (row[barcodeIndex] || "").trim(),
household_id: req.user.household_id
}); });
}); });
@@ -123,7 +129,7 @@ class ProductController {
editForm(req, res) { editForm(req, res) {
const productId = Number.parseInt(req.params.id, 10); const productId = Number.parseInt(req.params.id, 10);
const product = Number.isInteger(productId) const product = Number.isInteger(productId)
? ProductService.getProduct(productId) ? ProductService.getProduct(productId, req.user.household_id)
: null; : null;
if (!product) { if (!product) {
@@ -131,9 +137,7 @@ class ProductController {
return res.redirect("/products"); return res.redirect("/products");
} }
const categories = db.prepare( const categories = CategoryService.getCategories(req.user.household_id);
"SELECT * FROM categories ORDER BY name"
).all();
res.render("products/edit", { res.render("products/edit", {
title: "Product wijzigen", title: "Product wijzigen",
@@ -156,7 +160,12 @@ class ProductController {
return res.redirect("/products"); return res.redirect("/products");
} }
const result = ProductService.updateProduct(productId, { if (categoryId !== null && !CategoryService.getCategory(categoryId, req.user.household_id)) {
req.flash("error", "De gekozen categorie bestaat niet.");
return res.redirect(`/products/${productId}/edit`);
}
const result = ProductService.updateProduct(productId, req.user.household_id, {
name, name,
category_id: categoryId category_id: categoryId
}); });
@@ -179,7 +188,7 @@ class ProductController {
return res.redirect("/products"); return res.redirect("/products");
} }
if (ProductService.productHasListItems(productId)) { if (ProductService.productHasListItems(productId, req.user.household_id)) {
req.flash( req.flash(
"error", "error",
"Dit product staat nog op een boodschappenlijst en kan niet worden verwijderd." "Dit product staat nog op een boodschappenlijst en kan niet worden verwijderd."
@@ -187,7 +196,7 @@ class ProductController {
return res.redirect(`/products/${productId}/edit`); return res.redirect(`/products/${productId}/edit`);
} }
const result = ProductService.deleteProduct(productId); const result = ProductService.deleteProduct(productId, req.user.household_id);
if (result.changes === 0) { if (result.changes === 0) {
req.flash("error", "Product niet gevonden."); req.flash("error", "Product niet gevonden.");
+5 -3
View File
@@ -25,8 +25,8 @@ class ShoppingListController {
res.locals.title = list.name; res.locals.title = list.name;
res.render("lists/show", { res.render("lists/show", {
list, list,
items: ShoppingListRepository.getItems(list.id), items: ShoppingListRepository.getItems(list.id, req.user.household_id),
products: ProductRepository.getAll(), products: ProductRepository.getAll(req.user.household_id),
user: req.user user: req.user
}); });
} }
@@ -35,7 +35,9 @@ class ShoppingListController {
const list = ShoppingListRepository.findById(req.params.id, req.user.household_id); const list = ShoppingListRepository.findById(req.params.id, req.user.household_id);
const productId = Number.parseInt(req.body.product_id, 10); const productId = Number.parseInt(req.body.product_id, 10);
const amount = Number.parseFloat(req.body.amount || "1"); const amount = Number.parseFloat(req.body.amount || "1");
const product = Number.isInteger(productId) ? ProductRepository.findById(productId) : null; const product = Number.isInteger(productId)
? ProductRepository.findById(productId, req.user.household_id)
: null;
if (!list || !product || !Number.isFinite(amount) || amount <= 0) { if (!list || !product || !Number.isFinite(amount) || amount <= 0) {
req.flash("error", "Ongeldig product of ongeldige hoeveelheid"); req.flash("error", "Ongeldig product of ongeldige hoeveelheid");
return res.redirect(`/lists/${req.params.id}`); return res.redirect(`/lists/${req.params.id}`);
Binary file not shown.
Binary file not shown.
+18
View File
@@ -210,5 +210,23 @@ CREATE TABLE IF NOT EXISTS product_stores (
`); `);
const addColumnIfMissing = (table, column, definition) => {
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
if (!columns.some(existing => existing.name === column)) {
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
}
};
addColumnIfMissing("categories", "household_id", "INTEGER REFERENCES households(id)");
addColumnIfMissing("products", "household_id", "INTEGER REFERENCES households(id)");
const defaultHousehold = db.prepare("SELECT id FROM households ORDER BY id LIMIT 1").get();
if (defaultHousehold) {
db.prepare("UPDATE categories SET household_id = ? WHERE household_id IS NULL")
.run(defaultHousehold.id);
db.prepare("UPDATE products SET household_id = ? WHERE household_id IS NULL")
.run(defaultHousehold.id);
}
console.log("Database migratie voltooid."); console.log("Database migratie voltooid.");
+15 -6
View File
@@ -19,24 +19,33 @@ const products = [
["WC papier", "Huishouden"] ["WC papier", "Huishouden"]
]; ];
const household = db.prepare(
"SELECT id FROM households ORDER BY id LIMIT 1"
).get();
if (!household) {
console.log("Seed overgeslagen: er is nog geen huishouden.");
process.exit(0);
}
const insertCategory = db.prepare(` const insertCategory = db.prepare(`
INSERT OR IGNORE INTO categories (name, icon) VALUES (?, ?) INSERT OR IGNORE INTO categories (name, icon, household_id) VALUES (?, ?, ?)
`); `);
const insertProduct = db.prepare(` const insertProduct = db.prepare(`
INSERT OR IGNORE INTO products (name, category_id) VALUES (?, ?) INSERT OR IGNORE INTO products (name, category_id, household_id) VALUES (?, ?, ?)
`); `);
db.transaction(() => { db.transaction(() => {
for (const category of categories) { for (const category of categories) {
insertCategory.run(category); insertCategory.run(category[0], category[1], household.id);
} }
for (const [name, categoryName] of products) { for (const [name, categoryName] of products) {
const category = db.prepare( const category = db.prepare(
"SELECT id FROM categories WHERE name = ?" "SELECT id FROM categories WHERE name = ? AND household_id = ?"
).get(categoryName); ).get(categoryName, household.id);
insertProduct.run(name, category.id); insertProduct.run(name, category.id, household.id);
} }
})(); })();
+20 -17
View File
@@ -4,7 +4,7 @@ const db = require("../config/database");
class CategoryRepository { class CategoryRepository {
getAll() { getAll(householdId) {
return db.prepare(` return db.prepare(`
@@ -13,23 +13,26 @@ class CategoryRepository {
FROM categories FROM categories
LEFT JOIN products ON products.category_id = categories.id LEFT JOIN products ON products.category_id = categories.id
AND products.household_id = categories.household_id
WHERE categories.household_id = ?
GROUP BY categories.id GROUP BY categories.id
ORDER BY categories.name ORDER BY categories.name
`).all(); `).all(householdId);
} }
findById(id) { findById(id, householdId) {
return db.prepare(` return db.prepare(`
SELECT * FROM categories WHERE id = ? SELECT * FROM categories WHERE id = ? AND household_id = ?
`).get(id); `).get(id, householdId);
} }
@@ -38,16 +41,16 @@ class CategoryRepository {
return db.prepare(` return db.prepare(`
INSERT INTO categories (name, icon) INSERT INTO categories (name, icon, household_id)
VALUES (?, ?) VALUES (?, ?, ?)
`).run(data.name, data.icon || null); `).run(data.name, data.icon || null, data.household_id);
} }
update(id, data) { update(id, householdId, data) {
return db.prepare(` return db.prepare(`
@@ -55,31 +58,31 @@ class CategoryRepository {
SET name = ?, icon = ? SET name = ?, icon = ?
WHERE id = ? WHERE id = ? AND household_id = ?
`).run(data.name, data.icon || null, id); `).run(data.name, data.icon || null, id, householdId);
} }
hasProducts(id) { hasProducts(id, householdId) {
return db.prepare(` return db.prepare(`
SELECT 1 FROM products WHERE category_id = ? LIMIT 1 SELECT 1 FROM products WHERE category_id = ? AND household_id = ? LIMIT 1
`).get(id) !== undefined; `).get(id, householdId) !== undefined;
} }
delete(id) { delete(id, householdId) {
return db.prepare(` return db.prepare(`
DELETE FROM categories WHERE id = ? DELETE FROM categories WHERE id = ? AND household_id = ?
`).run(id); `).run(id, householdId);
} }
+30 -20
View File
@@ -4,7 +4,7 @@ const db = require("../config/database");
class ProductRepository { class ProductRepository {
getAll() { getAll(householdId) {
return db.prepare(` return db.prepare(`
@@ -24,16 +24,18 @@ class ProductRepository {
ON categories.id = products.category_id ON categories.id = products.category_id
WHERE products.household_id = ?
ORDER BY products.name ORDER BY products.name
`).all(); `).all(householdId);
} }
findById(id) { findById(id, householdId) {
return db.prepare(` return db.prepare(`
@@ -41,9 +43,9 @@ class ProductRepository {
FROM products FROM products
WHERE id = ? WHERE id = ? AND household_id = ?
`).get(id); `).get(id, householdId);
} }
@@ -59,10 +61,11 @@ class ProductRepository {
( (
name, name,
category_id, category_id,
barcode barcode,
household_id
) )
VALUES (?, ?, ?) VALUES (?, ?, ?, ?)
`).run( `).run(
@@ -70,7 +73,8 @@ class ProductRepository {
data.category_id || null, data.category_id || null,
data.barcode || null data.barcode || null,
data.household_id
); );
@@ -81,9 +85,9 @@ class ProductRepository {
createMany(products) { createMany(products) {
const insert = db.prepare(` const insert = db.prepare(`
INSERT INTO products (name, category_id, barcode) INSERT INTO products (name, category_id, barcode, household_id)
VALUES (?, ?, ?) VALUES (?, ?, ?, ?)
`); `);
@@ -92,7 +96,8 @@ class ProductRepository {
insert.run( insert.run(
product.name, product.name,
product.category_id || null, product.category_id || null,
product.barcode || null product.barcode || null,
product.household_id
); );
} }
}); });
@@ -102,7 +107,7 @@ class ProductRepository {
} }
update(id, data) { update(id, householdId, data) {
return db.prepare(` return db.prepare(`
@@ -110,7 +115,7 @@ class ProductRepository {
SET name = ?, category_id = ? SET name = ?, category_id = ?
WHERE id = ? WHERE id = ? AND household_id = ?
`).run( `).run(
@@ -118,14 +123,15 @@ class ProductRepository {
data.category_id || null, data.category_id || null,
id id,
householdId
); );
} }
hasListItems(id) { hasListItems(id, householdId) {
return db.prepare(` return db.prepare(`
@@ -133,24 +139,28 @@ class ProductRepository {
FROM shopping_list_items FROM shopping_list_items
WHERE product_id = ? WHERE product_id = ? AND EXISTS (
SELECT 1 FROM shopping_lists
WHERE shopping_lists.id = shopping_list_items.list_id
AND shopping_lists.household_id = ?
)
LIMIT 1 LIMIT 1
`).get(id) !== undefined; `).get(id, householdId) !== undefined;
} }
delete(id) { delete(id, householdId) {
return db.prepare(` return db.prepare(`
DELETE FROM products DELETE FROM products
WHERE id = ? WHERE id = ? AND household_id = ?
`).run(id); `).run(id, householdId);
} }
+3 -2
View File
@@ -22,7 +22,7 @@ class ShoppingListRepository {
`).get(id, householdId); `).get(id, householdId);
} }
getItems(listId) { getItems(listId, householdId) {
return db.prepare(` return db.prepare(`
SELECT shopping_list_items.*, products.name AS product_name, SELECT shopping_list_items.*, products.name AS product_name,
categories.name AS category_name, categories.icon AS category_icon categories.name AS category_name, categories.icon AS category_icon
@@ -30,10 +30,11 @@ class ShoppingListRepository {
JOIN products ON products.id = shopping_list_items.product_id JOIN products ON products.id = shopping_list_items.product_id
LEFT JOIN categories ON categories.id = products.category_id LEFT JOIN categories ON categories.id = products.category_id
WHERE shopping_list_items.list_id = ? WHERE shopping_list_items.list_id = ?
AND products.household_id = ?
ORDER BY shopping_list_items.checked ASC, ORDER BY shopping_list_items.checked ASC,
shopping_list_items.sort_order ASC, shopping_list_items.sort_order ASC,
shopping_list_items.created_at ASC shopping_list_items.created_at ASC
`).all(listId); `).all(listId, householdId);
} }
create(householdId, name) { create(householdId, name) {
+10 -10
View File
@@ -4,16 +4,16 @@ const CategoryRepository = require("../repositories/CategoryRepository");
class CategoryService { class CategoryService {
getCategories() { getCategories(householdId) {
return CategoryRepository.getAll(); return CategoryRepository.getAll(householdId);
} }
getCategory(id) { getCategory(id, householdId) {
return CategoryRepository.findById(id); return CategoryRepository.findById(id, householdId);
} }
@@ -25,23 +25,23 @@ class CategoryService {
} }
updateCategory(id, data) { updateCategory(id, householdId, data) {
return CategoryRepository.update(id, data); return CategoryRepository.update(id, householdId, data);
} }
categoryHasProducts(id) { categoryHasProducts(id, householdId) {
return CategoryRepository.hasProducts(id); return CategoryRepository.hasProducts(id, householdId);
} }
deleteCategory(id) { deleteCategory(id, householdId) {
return CategoryRepository.delete(id); return CategoryRepository.delete(id, householdId);
} }
+5 -5
View File
@@ -82,9 +82,9 @@ class MenuService {
} }
getNewProducts(menu) { getNewProducts(menu, householdId) {
const existingNames = new Set( const existingNames = new Set(
ProductRepository.getAll().map(product => normalizeName(product.name)) ProductRepository.getAll(householdId).map(product => normalizeName(product.name))
); );
const seen = new Set(); const seen = new Set();
const newProducts = []; const newProducts = [];
@@ -104,12 +104,12 @@ class MenuService {
addToShoppingList(householdId, name, menu) { addToShoppingList(householdId, name, menu) {
const products = ProductRepository.getAll(); const products = ProductRepository.getAll(householdId);
const productsByName = new Map( const productsByName = new Map(
products.map(product => [normalizeName(product.name), product]) products.map(product => [normalizeName(product.name), product])
); );
const insertProduct = db.prepare(` const insertProduct = db.prepare(`
INSERT INTO products (name, category_id, barcode) VALUES (?, NULL, NULL) INSERT INTO products (name, category_id, barcode, household_id) VALUES (?, NULL, NULL, ?)
`); `);
const insertList = db.prepare(` const insertList = db.prepare(`
INSERT INTO shopping_lists (household_id, name) VALUES (?, ?) INSERT INTO shopping_lists (household_id, name) VALUES (?, ?)
@@ -132,7 +132,7 @@ class MenuService {
let product = productsByName.get(key); let product = productsByName.get(key);
if (!product) { if (!product) {
const result = insertProduct.run(ingredient.name); const result = insertProduct.run(ingredient.name, householdId);
product = { id: result.lastInsertRowid, name: ingredient.name }; product = { id: result.lastInsertRowid, name: ingredient.name };
productsByName.set(key, product); productsByName.set(key, product);
} }
+10 -10
View File
@@ -5,9 +5,9 @@ require("../repositories/ProductRepository");
class ProductService { class ProductService {
getProducts() { getProducts(householdId) {
return ProductRepository.getAll(); return ProductRepository.getAll(householdId);
} }
@@ -26,30 +26,30 @@ class ProductService {
} }
getProduct(id) { getProduct(id, householdId) {
return ProductRepository.findById(id); return ProductRepository.findById(id, householdId);
} }
updateProduct(id, data) { updateProduct(id, householdId, data) {
return ProductRepository.update(id, data); return ProductRepository.update(id, householdId, data);
} }
productHasListItems(id) { productHasListItems(id, householdId) {
return ProductRepository.hasListItems(id); return ProductRepository.hasListItems(id, householdId);
} }
deleteProduct(id) { deleteProduct(id, householdId) {
return ProductRepository.delete(id); return ProductRepository.delete(id, householdId);
} }
BIN
View File
Binary file not shown.