Added actors admin panel with bulk merge. Fixed merge failing if source and target actor have conflicting network profiles.

This commit is contained in:
2026-06-17 00:05:42 +02:00
parent 994908ef6a
commit 721eaa5d07
28 changed files with 548 additions and 52 deletions

View File

@@ -378,7 +378,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
if (filters.query.charAt(0) === '#') {
builder.where('id', Number(escape(filters.query.slice(1))));
} else {
builder.whereRaw('match(\'@(name,aliases) :query:\', actors)', { query: escape(filters.query) });
builder.whereRaw(`match('@(name,aliases) :query:${filters.query.charAt(0) === '=' ? '' : '*'}', actors)`, { query: escape(filters.query) });
}
}
@@ -447,8 +447,13 @@ async function queryManticoreSql(filters, options, _reqUser) {
builder.where('has_avatar', 1);
}
console.log('ACTOR OPTIONS', options);
if (options.order?.[0] === 'name') {
builder.orderBy('actors.slug', options.order[1]);
builder.orderBy([
{ column: 'actors.slug', order: options.order[1] },
{ column: 'actors.entity_id', order: 'asc' },
]);
} else if (options.order?.[0] === 'likes') {
builder.orderBy([
{ column: 'actors.stashed', order: options.order[1] },
@@ -477,6 +482,10 @@ async function queryManticoreSql(filters, options, _reqUser) {
]);
} else {
builder.orderBy('actors.slug', 'asc');
builder.orderBy([
{ column: 'actors.slug', order: 'asc' },
{ column: 'actors.entity_id', order: 'asc' },
]);
}
})
.limit(options.limit)
@@ -558,72 +567,70 @@ export async function createActor(newActor, context, reqUser) {
return curateActor(actorEntry);
}
export async function mergeActors(targetActorId, sourceActorId, reqUser) {
export async function mergeActors(targetActorId, sourceActorIds, reqUser) {
if (!verifyAbility(reqUser, 'actor', 'merge')) {
throw new HttpError('You are not permitted to merge actors', 403);
}
const [targetActor, sourceActor] = await Promise.all([
if (sourceActorIds.includes(targetActorId)) {
throw new HttpError('Cannot merge actor profile into itself', 400);
}
const [targetActor, sourceActors] = await Promise.all([
knex('actors')
.where('id', targetActorId)
.whereNull('entity_id')
.whereNull('alias_for')
.first(),
knex('actors')
.where('id', sourceActorId)
.first(),
.whereIn('id', sourceActorIds),
]);
if (!targetActor) {
throw new HttpError('Target actor not found', 404);
}
if (!sourceActor) {
if (sourceActors.length < sourceActorIds.length) {
throw new HttpError('Source actor not found', 404);
}
if (targetActor.entity_id) {
throw new HttpError('Target actor is not global', 400);
}
if (targetActor.alias_for) {
throw new HttpError('Target actor is aliased', 400);
}
const trx = await knex.transaction();
let mergedProfiles;
let mergedScenes;
try {
await trx('actors')
.update('alias_for', targetActorId)
.where('id', sourceActorId)
.returning(['id', 'alias_for']);
const [existingProfiles] = await Promise.all([
trx('actors_profiles')
.where('actor_id', targetActorId),
trx('actors')
.update('alias_for', targetActorId)
.whereIn('id', sourceActorIds)
.returning(['id', 'alias_for']),
// some avatars are not matched to a profile, need to investigate why this happens and the avatar table needs a dedicated actor field
trx('actors_avatars')
.update('actor_id', targetActorId)
.whereIn('actor_id', sourceActorIds),
trx('stashes_actors')
.update('actor_id', targetActorId)
.whereIn('actor_id', sourceActorIds)
.returning('id'),
]);
mergedProfiles = await trx('actors_profiles')
.update('actor_id', targetActorId)
.where('actor_id', sourceActorId)
.whereIn('actor_id', sourceActorIds)
.whereNotIn('entity_id', existingProfiles.map((profile) => profile.entity_id))
.returning('id');
// some avatars are not matched to a profile, need to investigate why this happens and the avatar table needs a dedicated actor field
await trx('actors_avatars')
.update('actor_id', targetActorId)
.where('actor_id', sourceActorId);
mergedScenes = await trx('releases_actors')
.update({
actor_id: targetActorId,
alias_id: sourceActorId,
alias_id: knex.raw('actor_id'),
})
.where('actor_id', sourceActorId)
.whereIn('actor_id', sourceActorIds)
.returning('release_id');
await trx('stashes_actors')
.update('actor_id', targetActorId)
.where('actor_id', sourceActorId)
.returning('id');
await trx.commit();
} catch (error) {
await trx.rollback();
@@ -631,7 +638,7 @@ export async function mergeActors(targetActorId, sourceActorId, reqUser) {
throw error;
}
await interpolateProfiles([targetActorId, sourceActorId], {
await interpolateProfiles([targetActorId, ...sourceActorIds], {
knex,
logger,
moment,
@@ -641,8 +648,8 @@ export async function mergeActors(targetActorId, sourceActorId, reqUser) {
await Promise.all([
syncScenes(mergedScenes.map((scene) => scene.release_id)),
syncActors([targetActorId, sourceActorId]),
syncStashes('actor', [targetActorId, sourceActorId]),
syncActors([targetActorId, ...sourceActorIds]),
syncStashes('actor', [targetActorId, ...sourceActorIds]),
]);
return {