fix: prevent duplicate project resource assignments

With two tabs open (or two users) the same profession or company could
be assigned twice, because only the list loaded in the tab was checked.
Before creating an assignment the server is now asked whether it already
exists; if so its id is reused. Found in T-RES-03.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-16 12:33:38 +05:00
parent c69a11d167
commit 8ed1a02967
3 changed files with 46 additions and 7 deletions
@@ -3,6 +3,7 @@ import {
assignedItemsUrl,
assignedItemUrl,
deleteAssignedResource,
existingAssignmentUrl,
fetchAllItems,
fetchProjectResources,
poolItemsUrl,
@@ -105,13 +106,29 @@ describe('fetchProjectResources', () => {
});
describe('addAssignedResource', () => {
/** Проверка на дубль: привязки ещё нет. */
const notAssignedYet = { match: '$top=1', response: { body: { value: [] } } };
it('сначала ищет существующую привязку этого элемента к проекту', () => {
expect(existingAssignmentUrl(SITE, 'professions', '5', 12)).toEqual(
`${items('Профессии по проекту')}?$select=Id&$top=1&$filter=projectIdId eq 5 and professionIdId eq 12`
);
});
it('привязка уже есть (добавили в другой вкладке) — новая не создаётся, возвращается её Id', async () => {
const client = createFakeHttpClient([{ match: '$top=1', response: { body: { value: [{ Id: 77 }] } } }]);
expect(await addAssignedResource(client, SITE, 'professions', '5', { id: 12, title: 'Бетонщик' })).toEqual(77);
expect(client.requests.some((r) => r.request.method === 'POST')).toBe(false);
});
it('профессия: POST в items с телом как в assigner, возвращает Id', async () => {
const client = createFakeHttpClient([{ match: 'items', response: { status: 201, body: { Id: 101 } } }]);
const client = createFakeHttpClient([notAssignedYet, { match: 'items', response: { status: 201, body: { Id: 101 } } }]);
const id = await addAssignedResource(client, SITE, 'professions', '5', { id: 12, title: 'Бетонщик' });
expect(id).toEqual(101);
expect(client.requests[0]).toEqual({
expect(client.requests[1]).toEqual({
url: items('Профессии по проекту'),
request: {
method: 'POST',
@@ -126,16 +143,16 @@ describe('addAssignedResource', () => {
});
it('компания: поле companyIdId', async () => {
const client = createFakeHttpClient([{ match: 'items', response: { status: 201, body: { Id: 7 } } }]);
const client = createFakeHttpClient([notAssignedYet, { match: 'items', response: { status: 201, body: { Id: 7 } } }]);
await addAssignedResource(client, SITE, 'companies', '5', { id: 3, title: 'ТОО Строй' });
expect(client.requests[0].url).toEqual(items('Компании по проекту'));
expect(client.requests[0].request.body).toEqual('{"Title":"ТОО Строй","projectIdId":5,"companyIdId":3}');
expect(client.requests[1].url).toEqual(items('Компании по проекту'));
expect(client.requests[1].request.body).toEqual('{"Title":"ТОО Строй","projectIdId":5,"companyIdId":3}');
});
it('ошибка сервера пробрасывается', async () => {
const client = createFakeHttpClient([{ match: 'items', response: { status: 500, body: 'boom' } }]);
const client = createFakeHttpClient([notAssignedYet, { match: 'items', response: { status: 500, body: 'boom' } }]);
await expect(addAssignedResource(client, SITE, 'companies', '5', { id: 3, title: 'X' })).rejects.toThrow(
'500'
);
@@ -130,7 +130,20 @@ export async function fetchProjectResources(
};
}
/** Создаёт привязку и возвращает id новой записи. */
/** Уже существующая привязка этого элемента к проекту (`{поле}Id` — id из lookup). */
export function existingAssignmentUrl(siteUrl: string, kind: ResourceKind, projectId: string, itemId: number): string {
const { assignedList, lookupField } = RESOURCE_LISTS[kind];
return (
`${listItemsUrl(siteUrl, assignedList)}?$select=Id&$top=1` +
`&$filter=${PROJECT_LOOKUP_ID_FIELD} eq ${Number(projectId)} and ${lookupField}Id eq ${itemId}`
);
}
/**
* Создаёт привязку и возвращает её id. Если такая привязка уже есть (её создали в другой вкладке
* или другой пользователь, пока у нас был открыт старый список), новая не создаётся — возвращается
* id существующей. В старом assigner такой проверки не было, и получались дубли.
*/
export async function addAssignedResource(
client: IHttpClient,
siteUrl: string,
@@ -138,6 +151,14 @@ export async function addAssignedResource(
projectId: string,
item: IResourceItem
): Promise<number> {
const existing = await getJson<{ value: Array<{ Id: number }> }>(
client,
existingAssignmentUrl(siteUrl, kind, projectId, item.id)
);
if (existing.value.length > 0) {
return existing.value[0].Id;
}
const body = {
Title: item.title,
[PROJECT_LOOKUP_ID_FIELD]: Number(projectId),