Deduplicate seed data and add tests

This commit is contained in:
beatz174-bit
2025-11-24 07:20:55 +10:00
parent b01fc87403
commit 11db402bd3
2 changed files with 184 additions and 15 deletions
+121
View File
@@ -0,0 +1,121 @@
import { randomUUID } from 'crypto';
import { describe, expect, it } from 'vitest';
import { seedAreas, seedCategories, seedDatabase, seedProducts } from './seed';
import { Area } from '../models/Area';
import { Category } from '../models/Category';
import { DEFAULT_BULK_NAME, DEFAULT_UNIT_TYPE, Product } from '../models/Product';
import { StockFillDB } from './index';
const normalizeName = (name: string) => name.trim().toLowerCase();
class MockTable<T extends { id: string; name: string }> {
constructor(public items: T[] = []) {}
async count() {
return this.items.length;
}
async bulkAdd(records: T[]) {
this.items.push(...records);
}
async bulkDelete(ids: string[]) {
this.items = this.items.filter((item) => !ids.includes(item.id));
}
async toArray() {
return [...this.items];
}
}
const buildArea = (overrides: Partial<Area> = {}): Area => ({
id: overrides.id ?? randomUUID(),
name: overrides.name ?? 'Area',
created_at: overrides.created_at ?? Date.now(),
updated_at: overrides.updated_at ?? Date.now(),
});
const buildProduct = (overrides: Partial<Product> = {}): Product => ({
id: overrides.id ?? randomUUID(),
name: overrides.name ?? 'Product',
category: overrides.category ?? 'Category',
unit_type: overrides.unit_type ?? DEFAULT_UNIT_TYPE,
bulk_name: overrides.bulk_name ?? DEFAULT_BULK_NAME,
barcode: overrides.barcode,
archived: overrides.archived ?? false,
created_at: overrides.created_at ?? Date.now(),
updated_at: overrides.updated_at ?? Date.now(),
});
const buildCategory = (overrides: Partial<Category> = {}): Category => ({
id: overrides.id ?? randomUUID(),
name: overrides.name ?? 'Category',
created_at: overrides.created_at ?? Date.now(),
updated_at: overrides.updated_at ?? Date.now(),
});
const createMockDb = (options: {
areas?: Area[];
products?: Product[];
categories?: Category[];
} = {}) => {
const db = {
areas: new MockTable<Area>(options.areas ?? []),
products: new MockTable<Product>(options.products ?? []),
categories: new MockTable<Category>(options.categories ?? []),
pickLists: new MockTable<any>(),
pickItems: new MockTable<any>(),
} as unknown as StockFillDB;
return db;
};
describe('seedDatabase', () => {
it('deduplicates seeded areas, categories, and products', async () => {
const duplicateSeedArea = buildArea({ name: seedAreas[0] });
const trailingSpaceArea = buildArea({ name: `${seedAreas[0]} ` });
const customArea = buildArea({ name: 'Produce' });
const duplicateProduct = buildProduct({ name: seedProducts[0].name, category: seedProducts[0].category });
const duplicateProductWithWhitespace = buildProduct({ name: `${seedProducts[0].name} `, category: seedProducts[0].category });
const customProduct = buildProduct({ name: 'Custom Item', category: 'Specials' });
const duplicateCategory = buildCategory({ name: seedCategories[0] });
const trailingSpaceCategory = buildCategory({ name: `${seedCategories[0]} ` });
const db = createMockDb({
areas: [duplicateSeedArea, trailingSpaceArea, customArea],
products: [duplicateProduct, duplicateProductWithWhitespace, customProduct],
categories: [duplicateCategory, trailingSpaceCategory],
});
await seedDatabase(db);
const areas = await db.areas.toArray();
const areaNames = areas.map((area) => normalizeName(area.name));
const seededAreaNames = new Set(seedAreas.map(normalizeName));
expect(areaNames.filter((name) => name === normalizeName(seedAreas[0]))).toHaveLength(1);
expect(new Set(areaNames.filter((name) => seededAreaNames.has(name)))).toEqual(seededAreaNames);
expect(areaNames).toContain(normalizeName(customArea.name));
const products = await db.products.toArray();
const seededProductNames = new Set(seedProducts.map((product) => normalizeName(product.name)));
const productNamesInDb = products.map((product) => normalizeName(product.name));
expect(productNamesInDb.filter((name) => name === normalizeName(seedProducts[0].name))).toHaveLength(1);
expect(new Set(productNamesInDb.filter((name) => seededProductNames.has(name)))).toEqual(
seededProductNames,
);
expect(productNamesInDb).toContain(normalizeName(customProduct.name));
const categories = await db.categories.toArray();
const seededCategoryNames = new Set(seedCategories.map(normalizeName));
const categoryNamesInDb = categories.map((category) => normalizeName(category.name));
expect(categoryNamesInDb.filter((name) => name === normalizeName(seedCategories[0]))).toHaveLength(1);
expect(new Set(categoryNamesInDb.filter((name) => seededCategoryNames.has(name)))).toEqual(
seededCategoryNames,
);
});
});
+63 -15
View File
@@ -1,10 +1,13 @@
import { Table } from 'dexie';
import { v4 as uuidv4 } from 'uuid';
import { StockFillDB } from './index';
import { DEFAULT_BULK_NAME, DEFAULT_UNIT_TYPE } from '../models/Product';
const now = () => Date.now();
const seedProducts = [
export const seedAreas = ['Drinks', 'Snacks', 'Dairy'];
export const seedProducts = [
{ name: 'Nutrient Water Endurance', category: 'Drinks' },
{ name: 'Nutrient Water Focus', category: 'Drinks' },
{ name: 'Cocobella Choc', category: 'Drinks' },
@@ -33,10 +36,39 @@ const seedProducts = [
{ name: 'Mars Bar', category: 'Chocolates' },
];
const seedCategories = Array.from(
export const seedCategories = Array.from(
new Set(seedProducts.map(({ category }) => category)),
).sort();
const normalizeName = (name: string) => name.trim().toLowerCase();
const dedupeSeedRecords = async <T extends { id: string; name: string }>(
table: Table<T>,
seededNames: Set<string>,
) => {
const existing = await table.toArray();
const seen = new Set<string>();
const duplicateIds: string[] = [];
existing.forEach((record) => {
const normalized = normalizeName(record.name);
if (!seededNames.has(normalized)) return;
if (seen.has(normalized)) {
duplicateIds.push(record.id);
return;
}
seen.add(normalized);
});
if (duplicateIds.length > 0) {
await table.bulkDelete(duplicateIds);
}
return seen;
};
const buildProductRecord = (product: { name: string; category: string }) => ({
id: uuidv4(),
name: product.name,
@@ -49,19 +81,30 @@ const buildProductRecord = (product: { name: string; category: string }) => ({
});
export const seedDatabase = async (db: StockFillDB) => {
const areaCount = await db.areas.count();
if (areaCount === 0) {
await db.areas.bulkAdd([
{ id: uuidv4(), name: 'Drinks', created_at: now(), updated_at: now() },
{ id: uuidv4(), name: 'Snacks', created_at: now(), updated_at: now() },
{ id: uuidv4(), name: 'Dairy', created_at: now(), updated_at: now() },
]);
const seededAreaNames = new Set(seedAreas.map(normalizeName));
const existingSeedAreas = await dedupeSeedRecords(db.areas, seededAreaNames);
const missingAreas = seedAreas.filter((area) => !existingSeedAreas.has(normalizeName(area)));
if (missingAreas.length > 0) {
await db.areas.bulkAdd(
missingAreas.map((name) => ({
id: uuidv4(),
name,
created_at: now(),
updated_at: now(),
})),
);
}
const categoryCount = await db.categories.count();
if (categoryCount === 0) {
const seededCategoryNames = new Set(seedCategories.map(normalizeName));
const existingSeedCategories = await dedupeSeedRecords(db.categories, seededCategoryNames);
const missingCategories = seedCategories.filter(
(category) => !existingSeedCategories.has(normalizeName(category)),
);
if (missingCategories.length > 0) {
await db.categories.bulkAdd(
seedCategories.map((category) => ({
missingCategories.map((category) => ({
id: uuidv4(),
name: category,
created_at: now(),
@@ -70,8 +113,13 @@ export const seedDatabase = async (db: StockFillDB) => {
);
}
const productCount = await db.products.count();
if (productCount === 0) {
await db.products.bulkAdd(seedProducts.map(buildProductRecord));
const seededProductNames = new Set(seedProducts.map(({ name }) => normalizeName(name)));
const existingSeedProducts = await dedupeSeedRecords(db.products, seededProductNames);
const missingProducts = seedProducts.filter(
(product) => !existingSeedProducts.has(normalizeName(product.name)),
);
if (missingProducts.length > 0) {
await db.products.bulkAdd(missingProducts.map(buildProductRecord));
}
};