Commit 3dff6b5f authored by Antoine's avatar Antoine
Browse files

[server] Ensure duplicate budget items cannot be inserted in the db, and...

[server] Ensure duplicate budget items cannot be inserted in the db, and remove duplicate entries. Fix #1051.
parent f36d86c4
Pipeline #375570 passed with stage
in 4 minutes and 9 seconds
......@@ -439,7 +439,11 @@ export async function importData(userId: number, world: any) {
}
} else {
delete importedBudget.id;
await Budget.create(userId, importedBudget);
const newBudget = await Budget.create(userId, importedBudget);
// There could be duplicates in the import (see #1051), ensure we don't try
// to import the same budget twice.
existingBudgetsMap.set(makeBudgetKey(newBudget), newBudget);
}
}
log.info('Done.');
......
......@@ -6,6 +6,7 @@ import {
JoinColumn,
ManyToOne,
Repository,
Unique,
} from 'typeorm';
import User from './users';
......@@ -15,6 +16,7 @@ import { unwrap } from '../../helpers';
import { ForceNumericColumn } from '../helpers';
@Entity('budget')
@Unique(['userId', 'year', 'month', 'categoryId'])
export default class Budget {
private static REPO: Repository<Budget> | null = null;
......
import { MigrationInterface, QueryRunner } from 'typeorm';
import { Budget } from '..';
export class RemoveDuplicateBudgets1608817776804 implements MigrationInterface {
public async up(q: QueryRunner): Promise<void> {
const allBudgets = await q.manager.find(Budget);
const setOfUniqueBudgetKeys = new Set<string>();
const budgetIdsToDelete: number[] = [];
// Identify duplicate budget entries.
for (const budget of allBudgets) {
// The key to identify a budget uniquely.
const key = `${budget.userId}-${budget.year}-${budget.month}-${budget.categoryId}`;
if (setOfUniqueBudgetKeys.has(key)) {
budgetIdsToDelete.push(budget.id);
} else {
setOfUniqueBudgetKeys.add(key);
}
}
// Delete the duplicate entries.
if (budgetIdsToDelete.length) {
await q.manager.delete(Budget, budgetIdsToDelete);
}
}
public async down(): Promise<void> {
// Do nothing.
}
}
import { MigrationInterface, QueryRunner, TableUnique } from 'typeorm';
export class UniqueBudget1608817798703 implements MigrationInterface {
public async up(q: QueryRunner): Promise<void> {
// Add the unique constraint.
await q.createUniqueConstraint(
'budget',
new TableUnique({ columnNames: ['userId', 'year', 'month', 'categoryId'] })
);
}
public async down(q: QueryRunner): Promise<void> {
// Remove the unique constraint.
await q.dropUniqueConstraint(
'budget',
new TableUnique({ columnNames: ['userId', 'year', 'month', 'categoryId'] })
);
}
}
import should from 'should';
import { QueryFailedError } from 'typeorm';
import { Budget, Category } from '../../server/models';
describe('Duplicate budgets', () => {
let USER_ID = null;
before(() => {
// applyConfig must have already been called.
USER_ID = process.kresus.user.id;
});
it('Creating the same budget should raise.', async () => {
const category = await Category.create(USER_ID, { label: 'A label', color: '#000000' });
const dummyBudget = { categoryId: category.id, year: 2020, month: 12, threshold: 100 };
await Budget.create(USER_ID, dummyBudget);
await Budget.create(USER_ID, dummyBudget).should.be.rejectedWith(QueryFailedError);
});
});
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment