51 lines
1.8 KiB
TypeScript
51 lines
1.8 KiB
TypeScript
import type { DatabaseTransaction } from '@atlas/database';
|
|
import { ApiError } from './api-error.js';
|
|
|
|
/**
|
|
* Shared optimistic-lock conflict response. Versioned writes still keep their
|
|
* domain-specific SET clauses, but they must use a conditional `revision =
|
|
* expected` update and this helper when no row was affected.
|
|
*/
|
|
const resources = {
|
|
collection_item: {
|
|
table: 'atlas.collection_items',
|
|
label: '采集记录',
|
|
active: ' AND deleted_at IS NULL',
|
|
},
|
|
product: { table: 'atlas.products', label: '商品', active: '' },
|
|
product_platform_profile: {
|
|
table: 'atlas.product_platform_profiles',
|
|
label: '商品发布配置',
|
|
active: '',
|
|
},
|
|
} as const;
|
|
|
|
export async function throwRevisionConflict(
|
|
transaction: DatabaseTransaction,
|
|
resource: keyof typeof resources,
|
|
id: string,
|
|
expected: number,
|
|
): Promise<never> {
|
|
const target = resources[resource];
|
|
const current = await transaction.query<{ revision: number }>(
|
|
`SELECT revision FROM ${target.table} WHERE id = $1${target.active}`,
|
|
[id],
|
|
);
|
|
if (!current.rows[0])
|
|
throw new ApiError(404, 'RESOURCE_NOT_FOUND', `${target.label}不存在`);
|
|
if (current.rows[0].revision !== expected) {
|
|
throw new ApiError(409, 'VERSION_CONFLICT', `${target.label}已被更新`, [
|
|
{
|
|
field: 'If-Match',
|
|
code: 'VERSION_CONFLICT',
|
|
message: `当前版本为 ${current.rows[0].revision}`,
|
|
current_revision: current.rows[0].revision,
|
|
},
|
|
]);
|
|
}
|
|
// A conditional write with a matching version affecting no rows is a
|
|
// resource-state error (for example, a concurrent soft delete), not a
|
|
// successful retry.
|
|
throw new ApiError(409, 'VERSION_CONFLICT', `${target.label}状态已变化`);
|
|
}
|