Adding revisions for actor merges.
This commit is contained in:
@@ -128,6 +128,14 @@ const curatedRevisions = computed(() => revisions.value.map((revision) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (delta.key === 'mergeTarget' || delta.key === 'mergeSource') {
|
||||
// new socials don't have IDs yet, so we need to compare the values
|
||||
return {
|
||||
...delta,
|
||||
value: `#${delta.actorId} ${delta.actorName} (${delta.profileIds.length} profiles${delta.profileIds.length > 0 ? `: ${delta.profileIds.join()}` : ''})`,
|
||||
};
|
||||
}
|
||||
|
||||
if (dateKeys.includes(delta.key) && delta.value) {
|
||||
return {
|
||||
...delta,
|
||||
|
||||
477
src/actors.js
477
src/actors.js
@@ -597,6 +597,205 @@ function curateActorEntry(actor, context) {
|
||||
};
|
||||
}
|
||||
|
||||
function getBaseActor(actor) {
|
||||
return Object.fromEntries(Object.entries(actor).map(([key, values]) => {
|
||||
if ([
|
||||
'scenes',
|
||||
'likes',
|
||||
'stashes',
|
||||
'profiles',
|
||||
].includes(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ([
|
||||
'socials',
|
||||
].includes(key)) {
|
||||
return [key, values];
|
||||
}
|
||||
|
||||
if (values?.id) {
|
||||
return [key, values.id];
|
||||
}
|
||||
|
||||
if (values?.metric) {
|
||||
return [key, values.metric];
|
||||
}
|
||||
|
||||
if (Array.isArray(values)) {
|
||||
return [key, values.map((value) => value?.hash || value?.id || value)];
|
||||
}
|
||||
|
||||
return [key, values];
|
||||
}).filter(Boolean));
|
||||
}
|
||||
|
||||
function getDeltas(edits, baseActor, options) {
|
||||
return Promise.all(Object.entries(edits).map(async ([key, value]) => {
|
||||
if (baseActor[key] === value || typeof value === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key === 'originCountry' && edits.originPlace) {
|
||||
// place overrides country
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key === 'residenceCountry' && edits.residencePlace) {
|
||||
// place overrides country
|
||||
return null;
|
||||
}
|
||||
|
||||
if (['originPlace', 'residencePlace'].includes(key)) {
|
||||
if (!value && !baseActor[key]) {
|
||||
// don't pollute deltas if value is already unset
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return [
|
||||
// { key: key.includes('origin') ? 'originCountry' : 'residenceCountry', value: null },
|
||||
{ key: key.includes('origin') ? 'originState' : 'residenceState', value: null },
|
||||
{ key: key.includes('origin') ? 'originCity' : 'residenceCity', value: null },
|
||||
];
|
||||
}
|
||||
|
||||
const resolvedLocation = await resolvePlace(value, {
|
||||
knex,
|
||||
redis,
|
||||
logger,
|
||||
slugify,
|
||||
unprint,
|
||||
}, {
|
||||
userAgent: 'contact via https://traxxx.me/',
|
||||
});
|
||||
|
||||
if (!resolvedLocation) {
|
||||
throw new Error(`Failed to resolve ${key} ${value}`);
|
||||
}
|
||||
|
||||
const countryKey = key.includes('origin') ? 'originCountry' : 'residenceCountry';
|
||||
|
||||
if (!resolvedLocation.country) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
key: countryKey,
|
||||
value: resolvedLocation.country,
|
||||
comment: edits[countryKey] && edits[countryKey] !== resolvedLocation.country
|
||||
? `${countryKey} overridden by resolved ${key}`
|
||||
: null,
|
||||
},
|
||||
{
|
||||
key: key.includes('origin') ? 'originState' : 'residenceState',
|
||||
value: resolvedLocation.state || null, // explicitly unset to prevent outcomes like Los Angeles, Greenland
|
||||
},
|
||||
{
|
||||
key: key.includes('origin') ? 'originCity' : 'residenceCity',
|
||||
value: resolvedLocation.city || null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (key === 'socials') {
|
||||
const convertedSocials = curateSocials(value);
|
||||
|
||||
const convertedUrls = value
|
||||
.filter((social) => social.url && !convertedSocials.some((convertedSocial) => convertedSocial.url === social.url))
|
||||
.map((social) => social.url);
|
||||
|
||||
const conversionComment = convertedUrls.length > 0
|
||||
? `curated URLs ${convertedUrls.join(', ')} as social handles`
|
||||
: null;
|
||||
|
||||
return {
|
||||
key,
|
||||
value: convertedSocials,
|
||||
comment: conversionComment,
|
||||
};
|
||||
}
|
||||
|
||||
if (['cup', 'bust', 'waist', 'hip'].includes(key)) {
|
||||
const convertedValue = convertFigure(key, value, options.figureUnits);
|
||||
|
||||
const conversionComment = !value || convertedValue === value
|
||||
? null
|
||||
: `${key} converted from ${value} ${options.figureUnits?.toUpperCase() || 'US'} to ${convertedValue} US`;
|
||||
|
||||
return {
|
||||
key,
|
||||
value: convertedValue,
|
||||
comment: conversionComment,
|
||||
};
|
||||
}
|
||||
|
||||
if (['height'].includes(key)) {
|
||||
const convertedValue = convertHeight(value, options.sizeUnits);
|
||||
|
||||
if (baseActor[key] === convertedValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const conversionComment = !value || convertedValue === value
|
||||
? null
|
||||
: `${key} converted from ${value[0]} in ${value[1]} ft to ${convertedValue} cm`;
|
||||
|
||||
return {
|
||||
key,
|
||||
value: convertedValue,
|
||||
comment: conversionComment,
|
||||
};
|
||||
}
|
||||
|
||||
if (['weight'].includes(key)) {
|
||||
const convertedValue = convertWeight(value, options.sizeUnits);
|
||||
|
||||
if (baseActor[key] === convertedValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const conversionComment = !value || convertedValue === value
|
||||
? null
|
||||
: `${key} converted from ${value} lbs to ${convertedValue} kg`;
|
||||
|
||||
return {
|
||||
key,
|
||||
value: convertedValue,
|
||||
comment: conversionComment,
|
||||
};
|
||||
}
|
||||
|
||||
if (['penisLength', 'penisGirth'].includes(key) && options.penisUnits === 'imperial') {
|
||||
const convertedValue = Math.round(convert(value, 'inches').to('cm'));
|
||||
|
||||
if (baseActor[key] === convertedValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
value: convertedValue,
|
||||
comment: `${key} converted from ${value} inches to ${convertedValue} cm`,
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const valueSet = new Set(value);
|
||||
const baseSet = new Set(baseActor[key]);
|
||||
|
||||
if (valueSet.size === baseSet.size && baseActor[key].every((id) => valueSet.has(id))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { key, value: Array.from(valueSet) };
|
||||
}
|
||||
|
||||
return { key, value };
|
||||
})).then((rawDeltas) => rawDeltas.flat().filter(Boolean));
|
||||
}
|
||||
|
||||
export async function createActor(newActor, context, reqUser) {
|
||||
if (!reqUser || reqUser.role === 'user') {
|
||||
throw new HttpError('You are not permitted to create actors', 403);
|
||||
@@ -619,7 +818,7 @@ export async function mergeActors(targetActorId, sourceActorIds, reqUser) {
|
||||
throw new HttpError('Cannot merge actor profile into itself', 400);
|
||||
}
|
||||
|
||||
const [targetActor, sourceActors] = await Promise.all([
|
||||
const [targetActorEntry, sourceActorEntries] = await Promise.all([
|
||||
knex('actors')
|
||||
.where('id', targetActorId)
|
||||
.whereNull('entity_id')
|
||||
@@ -629,14 +828,27 @@ export async function mergeActors(targetActorId, sourceActorIds, reqUser) {
|
||||
.whereIn('id', sourceActorIds),
|
||||
]);
|
||||
|
||||
if (!targetActor) {
|
||||
if (!targetActorEntry) {
|
||||
throw new HttpError('Target actor not found', 404);
|
||||
}
|
||||
|
||||
if (sourceActors.length < sourceActorIds.length) {
|
||||
if (sourceActorEntries.length < sourceActorIds.length) {
|
||||
throw new HttpError('Source actor not found', 404);
|
||||
}
|
||||
|
||||
const [[targetActor], sourceActors] = await Promise.all([
|
||||
fetchActorsById([targetActorEntry.id], {
|
||||
reqUser,
|
||||
includeAssets: true,
|
||||
includePartOf: true,
|
||||
}),
|
||||
fetchActorsById(sourceActorEntries.map((sourceActorEntry) => sourceActorEntry.id), {
|
||||
reqUser,
|
||||
includeAssets: true,
|
||||
includePartOf: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const trx = await knex.transaction();
|
||||
|
||||
let mergedProfiles = [];
|
||||
@@ -674,7 +886,7 @@ export async function mergeActors(targetActorId, sourceActorIds, reqUser) {
|
||||
.whereIn('actor_id', sourceActorIds)
|
||||
.whereNotIn('entity_id', existingProfiles.map((profile) => profile.entity_id))
|
||||
.whereNotIn('id', duplicateSourceProfiles.map((profile) => profile.id))
|
||||
.returning('id');
|
||||
.returning(['id', 'actor_id']);
|
||||
|
||||
// find releases that have more than one source actor assigned
|
||||
duplicateSourceActors = await trx('releases_actors')
|
||||
@@ -748,11 +960,61 @@ export async function mergeActors(targetActorId, sourceActorIds, reqUser) {
|
||||
.whereIn('actor_id', sourceActorIds)
|
||||
.returning('stash_id');
|
||||
|
||||
/* TODO: add revision entries
|
||||
console.log('SOURCES', targetActor, sourceActors);
|
||||
const baseTargetActor = getBaseActor(targetActor);
|
||||
|
||||
throw new Error('ABORT');
|
||||
*/
|
||||
const targetDeltas = sourceActors.map((sourceActor) => ({
|
||||
key: 'mergeSource',
|
||||
actorId: sourceActor.id,
|
||||
actorName: sourceActor.name,
|
||||
profileIds: sourceProfiles
|
||||
.filter((sourceProfile) => sourceProfile.actor_id === sourceActor.id)
|
||||
.map((sourceProfile) => sourceProfile.id),
|
||||
}));
|
||||
|
||||
await trx('actors_revisions')
|
||||
.insert({
|
||||
actor_id: targetActor.id,
|
||||
user_id: reqUser.id,
|
||||
base: baseTargetActor,
|
||||
deltas: JSON.stringify(targetDeltas),
|
||||
hash: mj.hash({
|
||||
base: baseTargetActor,
|
||||
deltas: targetDeltas,
|
||||
}),
|
||||
reviewed_by: reqUser.id,
|
||||
reviewed_at: knex.fn.now(),
|
||||
applied_at: knex.fn.now(),
|
||||
approved: true,
|
||||
});
|
||||
|
||||
await trx('actors_revisions')
|
||||
.insert(sourceActors.map((sourceActor) => {
|
||||
const baseSourceActor = getBaseActor(sourceActor);
|
||||
|
||||
const deltas = [{
|
||||
key: 'mergeTarget',
|
||||
actorId: targetActorId,
|
||||
actorName: targetActor.name,
|
||||
profileIds: sourceProfiles
|
||||
.filter((sourceProfile) => sourceProfile.actor_id === sourceActor.id)
|
||||
.map((sourceProfile) => sourceProfile.id),
|
||||
}];
|
||||
|
||||
return {
|
||||
actor_id: sourceActor.id,
|
||||
user_id: reqUser.id,
|
||||
base: baseSourceActor,
|
||||
deltas: JSON.stringify(deltas),
|
||||
hash: mj.hash({
|
||||
base: baseSourceActor,
|
||||
deltas: targetDeltas,
|
||||
}),
|
||||
reviewed_by: reqUser.id,
|
||||
reviewed_at: knex.fn.now(),
|
||||
applied_at: knex.fn.now(),
|
||||
approved: true,
|
||||
};
|
||||
}));
|
||||
|
||||
await trx.commit();
|
||||
}
|
||||
@@ -1151,205 +1413,6 @@ function curateSocials(socials) {
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function getBaseActor(actor) {
|
||||
return Object.fromEntries(Object.entries(actor).map(([key, values]) => {
|
||||
if ([
|
||||
'scenes',
|
||||
'likes',
|
||||
'stashes',
|
||||
'profiles',
|
||||
].includes(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ([
|
||||
'socials',
|
||||
].includes(key)) {
|
||||
return [key, values];
|
||||
}
|
||||
|
||||
if (values?.id) {
|
||||
return [key, values.id];
|
||||
}
|
||||
|
||||
if (values?.metric) {
|
||||
return [key, values.metric];
|
||||
}
|
||||
|
||||
if (Array.isArray(values)) {
|
||||
return [key, values.map((value) => value?.hash || value?.id || value)];
|
||||
}
|
||||
|
||||
return [key, values];
|
||||
}).filter(Boolean));
|
||||
}
|
||||
|
||||
function getDeltas(edits, baseActor, options) {
|
||||
return Promise.all(Object.entries(edits).map(async ([key, value]) => {
|
||||
if (baseActor[key] === value || typeof value === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key === 'originCountry' && edits.originPlace) {
|
||||
// place overrides country
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key === 'residenceCountry' && edits.residencePlace) {
|
||||
// place overrides country
|
||||
return null;
|
||||
}
|
||||
|
||||
if (['originPlace', 'residencePlace'].includes(key)) {
|
||||
if (!value && !baseActor[key]) {
|
||||
// don't pollute deltas if value is already unset
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return [
|
||||
// { key: key.includes('origin') ? 'originCountry' : 'residenceCountry', value: null },
|
||||
{ key: key.includes('origin') ? 'originState' : 'residenceState', value: null },
|
||||
{ key: key.includes('origin') ? 'originCity' : 'residenceCity', value: null },
|
||||
];
|
||||
}
|
||||
|
||||
const resolvedLocation = await resolvePlace(value, {
|
||||
knex,
|
||||
redis,
|
||||
logger,
|
||||
slugify,
|
||||
unprint,
|
||||
}, {
|
||||
userAgent: 'contact via https://traxxx.me/',
|
||||
});
|
||||
|
||||
if (!resolvedLocation) {
|
||||
throw new Error(`Failed to resolve ${key} ${value}`);
|
||||
}
|
||||
|
||||
const countryKey = key.includes('origin') ? 'originCountry' : 'residenceCountry';
|
||||
|
||||
if (!resolvedLocation.country) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
key: countryKey,
|
||||
value: resolvedLocation.country,
|
||||
comment: edits[countryKey] && edits[countryKey] !== resolvedLocation.country
|
||||
? `${countryKey} overridden by resolved ${key}`
|
||||
: null,
|
||||
},
|
||||
{
|
||||
key: key.includes('origin') ? 'originState' : 'residenceState',
|
||||
value: resolvedLocation.state || null, // explicitly unset to prevent outcomes like Los Angeles, Greenland
|
||||
},
|
||||
{
|
||||
key: key.includes('origin') ? 'originCity' : 'residenceCity',
|
||||
value: resolvedLocation.city || null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (key === 'socials') {
|
||||
const convertedSocials = curateSocials(value);
|
||||
|
||||
const convertedUrls = value
|
||||
.filter((social) => social.url && !convertedSocials.some((convertedSocial) => convertedSocial.url === social.url))
|
||||
.map((social) => social.url);
|
||||
|
||||
const conversionComment = convertedUrls.length > 0
|
||||
? `curated URLs ${convertedUrls.join(', ')} as social handles`
|
||||
: null;
|
||||
|
||||
return {
|
||||
key,
|
||||
value: convertedSocials,
|
||||
comment: conversionComment,
|
||||
};
|
||||
}
|
||||
|
||||
if (['cup', 'bust', 'waist', 'hip'].includes(key)) {
|
||||
const convertedValue = convertFigure(key, value, options.figureUnits);
|
||||
|
||||
const conversionComment = !value || convertedValue === value
|
||||
? null
|
||||
: `${key} converted from ${value} ${options.figureUnits?.toUpperCase() || 'US'} to ${convertedValue} US`;
|
||||
|
||||
return {
|
||||
key,
|
||||
value: convertedValue,
|
||||
comment: conversionComment,
|
||||
};
|
||||
}
|
||||
|
||||
if (['height'].includes(key)) {
|
||||
const convertedValue = convertHeight(value, options.sizeUnits);
|
||||
|
||||
if (baseActor[key] === convertedValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const conversionComment = !value || convertedValue === value
|
||||
? null
|
||||
: `${key} converted from ${value[0]} in ${value[1]} ft to ${convertedValue} cm`;
|
||||
|
||||
return {
|
||||
key,
|
||||
value: convertedValue,
|
||||
comment: conversionComment,
|
||||
};
|
||||
}
|
||||
|
||||
if (['weight'].includes(key)) {
|
||||
const convertedValue = convertWeight(value, options.sizeUnits);
|
||||
|
||||
if (baseActor[key] === convertedValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const conversionComment = !value || convertedValue === value
|
||||
? null
|
||||
: `${key} converted from ${value} lbs to ${convertedValue} kg`;
|
||||
|
||||
return {
|
||||
key,
|
||||
value: convertedValue,
|
||||
comment: conversionComment,
|
||||
};
|
||||
}
|
||||
|
||||
if (['penisLength', 'penisGirth'].includes(key) && options.penisUnits === 'imperial') {
|
||||
const convertedValue = Math.round(convert(value, 'inches').to('cm'));
|
||||
|
||||
if (baseActor[key] === convertedValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
value: convertedValue,
|
||||
comment: `${key} converted from ${value} inches to ${convertedValue} cm`,
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const valueSet = new Set(value);
|
||||
const baseSet = new Set(baseActor[key]);
|
||||
|
||||
if (valueSet.size === baseSet.size && baseActor[key].every((id) => valueSet.has(id))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { key, value: Array.from(valueSet) };
|
||||
}
|
||||
|
||||
return { key, value };
|
||||
})).then((rawDeltas) => rawDeltas.flat().filter(Boolean));
|
||||
}
|
||||
|
||||
export async function createActorRevision(actorId, {
|
||||
edits,
|
||||
comment,
|
||||
|
||||
Reference in New Issue
Block a user