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", {
title: "Categorieën",
categories: CategoryService.getCategories()
categories: CategoryService.getCategories(req.user.household_id)
});
}
@@ -17,7 +17,7 @@ class CategoryController {
editForm(req, res) {
const categoryId = Number.parseInt(req.params.id, 10);
const category = Number.isInteger(categoryId)
? CategoryService.getCategory(categoryId)
? CategoryService.getCategory(categoryId, req.user.household_id)
: null;
if (!category) {
@@ -41,7 +41,7 @@ class CategoryController {
return res.redirect("/categories");
}
CategoryService.createCategory({ name, icon });
CategoryService.createCategory({ name, icon, household_id: req.user.household_id });
req.flash("success", "Categorie toegevoegd.");
res.redirect("/categories");
}
@@ -57,7 +57,7 @@ class CategoryController {
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) {
req.flash("error", "Categorie niet gevonden.");
@@ -77,7 +77,7 @@ class CategoryController {
return res.redirect("/categories");
}
if (CategoryService.categoryHasProducts(categoryId)) {
if (CategoryService.categoryHasProducts(categoryId, req.user.household_id)) {
req.flash(
"error",
"Deze categorie is nog aan producten gekoppeld en kan niet worden verwijderd."
@@ -85,7 +85,7 @@ class CategoryController {
return res.redirect("/categories");
}
const result = CategoryService.deleteCategory(categoryId);
const result = CategoryService.deleteCategory(categoryId, req.user.household_id);
if (result.changes === 0) {
req.flash("error", "Categorie niet gevonden.");
+1 -1
View File
@@ -25,7 +25,7 @@ class MenuController {
res.render("menu/preview", {
title: "Weekmenu controleren",
menu,
newProducts: MenuService.getNewProducts(menu),
newProducts: MenuService.getNewProducts(menu, req.user.household_id),
listName: "Weekboodschappen"
});
} catch (error) {
+24 -15
View File
@@ -1,7 +1,8 @@
const ProductService =
require("../services/ProductService");
const db = require("../config/database");
const CategoryService = require("../services/CategoryService");
const parseCsv = require("../utils/parseCsv");
class ProductController {
@@ -10,11 +11,9 @@ class ProductController {
index(req, res) {
const products =
ProductService.getProducts();
ProductService.getProducts(req.user.household_id);
const categories = db.prepare(
"SELECT * FROM categories ORDER BY name"
).all();
const categories = CategoryService.getCategories(req.user.household_id);
res.render(
"products/index",
@@ -39,9 +38,15 @@ class ProductController {
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({
name,
category_id: categoryId
category_id: categoryId,
household_id: req.user.household_id
});
req.flash("success", "Product toegevoegd.");
@@ -80,7 +85,7 @@ class ProductController {
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(
categories.map(category => [category.name.trim().toLowerCase(), category.id])
);
@@ -105,7 +110,8 @@ class ProductController {
products.push({
name,
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) {
const productId = Number.parseInt(req.params.id, 10);
const product = Number.isInteger(productId)
? ProductService.getProduct(productId)
? ProductService.getProduct(productId, req.user.household_id)
: null;
if (!product) {
@@ -131,9 +137,7 @@ class ProductController {
return res.redirect("/products");
}
const categories = db.prepare(
"SELECT * FROM categories ORDER BY name"
).all();
const categories = CategoryService.getCategories(req.user.household_id);
res.render("products/edit", {
title: "Product wijzigen",
@@ -156,7 +160,12 @@ class ProductController {
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,
category_id: categoryId
});
@@ -179,7 +188,7 @@ class ProductController {
return res.redirect("/products");
}
if (ProductService.productHasListItems(productId)) {
if (ProductService.productHasListItems(productId, req.user.household_id)) {
req.flash(
"error",
"Dit product staat nog op een boodschappenlijst en kan niet worden verwijderd."
@@ -187,7 +196,7 @@ class ProductController {
return res.redirect(`/products/${productId}/edit`);
}
const result = ProductService.deleteProduct(productId);
const result = ProductService.deleteProduct(productId, req.user.household_id);
if (result.changes === 0) {
req.flash("error", "Product niet gevonden.");
+5 -3
View File
@@ -25,8 +25,8 @@ class ShoppingListController {
res.locals.title = list.name;
res.render("lists/show", {
list,
items: ShoppingListRepository.getItems(list.id),
products: ProductRepository.getAll(),
items: ShoppingListRepository.getItems(list.id, req.user.household_id),
products: ProductRepository.getAll(req.user.household_id),
user: req.user
});
}
@@ -35,7 +35,9 @@ class ShoppingListController {
const list = ShoppingListRepository.findById(req.params.id, req.user.household_id);
const productId = Number.parseInt(req.body.product_id, 10);
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) {
req.flash("error", "Ongeldig product of ongeldige hoeveelheid");
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.");
+15 -6
View File
@@ -19,24 +19,33 @@ const products = [
["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(`
INSERT OR IGNORE INTO categories (name, icon) VALUES (?, ?)
INSERT OR IGNORE INTO categories (name, icon, household_id) VALUES (?, ?, ?)
`);
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(() => {
for (const category of categories) {
insertCategory.run(category);
insertCategory.run(category[0], category[1], household.id);
}
for (const [name, categoryName] of products) {
const category = db.prepare(
"SELECT id FROM categories WHERE name = ?"
).get(categoryName);
insertProduct.run(name, category.id);
"SELECT id FROM categories WHERE name = ? AND household_id = ?"
).get(categoryName, household.id);
insertProduct.run(name, category.id, household.id);
}
})();
+20 -17
View File
@@ -4,7 +4,7 @@ const db = require("../config/database");
class CategoryRepository {
getAll() {
getAll(householdId) {
return db.prepare(`
@@ -13,23 +13,26 @@ class CategoryRepository {
FROM categories
LEFT JOIN products ON products.category_id = categories.id
AND products.household_id = categories.household_id
WHERE categories.household_id = ?
GROUP BY categories.id
ORDER BY categories.name
`).all();
`).all(householdId);
}
findById(id) {
findById(id, householdId) {
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(`
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(`
@@ -55,31 +58,31 @@ class CategoryRepository {
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(`
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(`
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 {
getAll() {
getAll(householdId) {
return db.prepare(`
@@ -24,16 +24,18 @@ class ProductRepository {
ON categories.id = products.category_id
WHERE products.household_id = ?
ORDER BY products.name
`).all();
`).all(householdId);
}
findById(id) {
findById(id, householdId) {
return db.prepare(`
@@ -41,9 +43,9 @@ class ProductRepository {
FROM products
WHERE id = ?
WHERE id = ? AND household_id = ?
`).get(id);
`).get(id, householdId);
}
@@ -59,10 +61,11 @@ class ProductRepository {
(
name,
category_id,
barcode
barcode,
household_id
)
VALUES (?, ?, ?)
VALUES (?, ?, ?, ?)
`).run(
@@ -70,7 +73,8 @@ class ProductRepository {
data.category_id || null,
data.barcode || null
data.barcode || null,
data.household_id
);
@@ -81,9 +85,9 @@ class ProductRepository {
createMany(products) {
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(
product.name,
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(`
@@ -110,7 +115,7 @@ class ProductRepository {
SET name = ?, category_id = ?
WHERE id = ?
WHERE id = ? AND household_id = ?
`).run(
@@ -118,14 +123,15 @@ class ProductRepository {
data.category_id || null,
id
id,
householdId
);
}
hasListItems(id) {
hasListItems(id, householdId) {
return db.prepare(`
@@ -133,24 +139,28 @@ class ProductRepository {
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
`).get(id) !== undefined;
`).get(id, householdId) !== undefined;
}
delete(id) {
delete(id, householdId) {
return db.prepare(`
DELETE FROM products
WHERE id = ?
WHERE id = ? AND household_id = ?
`).run(id);
`).run(id, householdId);
}
+4 -3
View File
@@ -22,18 +22,19 @@ class ShoppingListRepository {
`).get(id, householdId);
}
getItems(listId) {
getItems(listId, householdId) {
return db.prepare(`
SELECT shopping_list_items.*, products.name AS product_name,
categories.name AS category_name, categories.icon AS category_icon
FROM shopping_list_items
JOIN products ON products.id = shopping_list_items.product_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,
shopping_list_items.sort_order ASC,
shopping_list_items.created_at ASC
`).all(listId);
`).all(listId, householdId);
}
create(householdId, name) {
+10 -10
View File
@@ -4,16 +4,16 @@ const CategoryRepository = require("../repositories/CategoryRepository");
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(
ProductRepository.getAll().map(product => normalizeName(product.name))
ProductRepository.getAll(householdId).map(product => normalizeName(product.name))
);
const seen = new Set();
const newProducts = [];
@@ -104,12 +104,12 @@ class MenuService {
addToShoppingList(householdId, name, menu) {
const products = ProductRepository.getAll();
const products = ProductRepository.getAll(householdId);
const productsByName = new Map(
products.map(product => [normalizeName(product.name), product])
);
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(`
INSERT INTO shopping_lists (household_id, name) VALUES (?, ?)
@@ -132,7 +132,7 @@ class MenuService {
let product = productsByName.get(key);
if (!product) {
const result = insertProduct.run(ingredient.name);
const result = insertProduct.run(ingredient.name, householdId);
product = { id: result.lastInsertRowid, name: ingredient.name };
productsByName.set(key, product);
}
+10 -10
View File
@@ -5,9 +5,9 @@ require("../repositories/ProductRepository");
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.