2024-07-18 21:35:17 +03:00
import sql from "./sql.js" ;
import dateUtils from "./date_utils.js" ;
import log from "./log.js" ;
import cls from "./cls.js" ;
2025-01-02 13:47:44 +01:00
import { randomString } from "./utils.js" ;
2024-07-18 21:35:17 +03:00
import instanceId from "./instance_id.js" ;
import becca from "../becca/becca.js" ;
import blobService from "../services/blob.js" ;
2025-01-09 18:36:24 +02:00
import type { EntityChange } from "./entity_changes_interface.js" ;
2024-07-24 20:27:50 +03:00
import type { Blob } from "./blob-interface.js" ;
2024-07-18 23:06:08 +03:00
import eventService from "./events.js" ;
2024-02-16 23:56:32 +02:00
let maxEntityChangeId = 0 ;
function putEntityChangeWithInstanceId ( origEntityChange : EntityChange , instanceId : string ) {
2025-01-09 18:07:02 +02:00
const ec = { . . . origEntityChange , instanceId } ;
2024-02-16 23:56:32 +02:00
putEntityChange ( ec ) ;
2024-02-16 21:16:35 +02:00
}
2024-02-16 23:56:32 +02:00
function putEntityChangeWithForcedChange ( origEntityChange : EntityChange ) {
2025-01-09 18:07:02 +02:00
const ec = { . . . origEntityChange , changeId : null } ;
2024-02-16 23:56:32 +02:00
putEntityChange ( ec ) ;
}
function putEntityChange ( origEntityChange : EntityChange ) {
2025-01-09 18:07:02 +02:00
const ec = { . . . origEntityChange } ;
2024-02-16 23:56:32 +02:00
delete ec . id ;
if ( ! ec . changeId ) {
2025-01-02 13:47:44 +01:00
ec . changeId = randomString ( 12 ) ;
2024-02-16 23:56:32 +02:00
}
ec . componentId = ec . componentId || cls . getComponentId ( ) || "NA" ; // NA = not available
ec . instanceId = ec . instanceId || instanceId ;
ec . isSynced = ec . isSynced ? 1 : 0 ;
ec . isErased = ec . isErased ? 1 : 0 ;
ec . id = sql . replace ( "entity_changes" , ec ) ;
2024-12-22 15:42:15 +02:00
if ( ec . id ) {
maxEntityChangeId = Math . max ( maxEntityChangeId , ec . id ) ;
}
2024-02-16 23:56:32 +02:00
cls . putEntityChange ( ec ) ;
}
2024-02-17 22:58:54 +02:00
function putNoteReorderingEntityChange ( parentNoteId : string , componentId? : string ) {
2024-02-16 23:56:32 +02:00
putEntityChange ( {
entityName : "note_reordering" ,
entityId : parentNoteId ,
2025-01-09 18:07:02 +02:00
hash : "N/A" ,
2024-02-16 23:56:32 +02:00
isErased : false ,
utcDateChanged : dateUtils.utcNowDateTime ( ) ,
isSynced : true ,
componentId ,
instanceId
} ) ;
eventService . emit ( eventService . ENTITY_CHANGED , {
2025-01-09 18:07:02 +02:00
entityName : "note_reordering" ,
2025-04-01 23:30:21 +03:00
entity : sql.getMap ( /*sql*/ ` SELECT branchId, notePosition FROM branches WHERE isDeleted = 0 AND parentNoteId = ? ` , [ parentNoteId ] )
2024-02-16 23:56:32 +02:00
} ) ;
}
function putEntityChangeForOtherInstances ( ec : EntityChange ) {
putEntityChange ( {
. . . ec ,
changeId : null ,
instanceId : null
} ) ;
}
function addEntityChangesForSector ( entityName : string , sector : string ) {
2025-04-01 23:30:21 +03:00
const entityChanges = sql . getRows < EntityChange > ( /*sql*/ ` SELECT * FROM entity_changes WHERE entityName = ? AND SUBSTR(entityId, 1, 1) = ? ` , [ entityName , sector ] ) ;
2024-02-16 23:56:32 +02:00
let entitiesInserted = entityChanges . length ;
sql . transactional ( ( ) = > {
2025-01-09 18:07:02 +02:00
if ( entityName === "blobs" ) {
entitiesInserted += addEntityChangesForDependingEntity ( sector , "notes" , "noteId" ) ;
entitiesInserted += addEntityChangesForDependingEntity ( sector , "attachments" , "attachmentId" ) ;
entitiesInserted += addEntityChangesForDependingEntity ( sector , "revisions" , "revisionId" ) ;
2024-02-16 23:56:32 +02:00
}
for ( const ec of entityChanges ) {
putEntityChangeWithForcedChange ( ec ) ;
}
} ) ;
log . info ( ` Added sector ${ sector } of ' ${ entityName } ' ( ${ entitiesInserted } entities) to the sync queue. ` ) ;
}
function addEntityChangesForDependingEntity ( sector : string , tableName : string , primaryKeyColumn : string ) {
// problem in blobs might be caused by problem in entity referencing the blob
2025-01-09 18:07:02 +02:00
const dependingEntityChanges = sql . getRows < EntityChange > (
`
2024-12-22 15:42:15 +02:00
SELECT dep_change . *
2024-02-16 23:56:32 +02:00
FROM entity_changes orig_sector
JOIN $ { tableName } ON $ { tableName } . blobId = orig_sector . entityId
JOIN entity_changes dep_change ON dep_change . entityName = '${tableName}' AND dep_change . entityId = $ { tableName } . $ { primaryKeyColumn }
2025-01-09 18:07:02 +02:00
WHERE orig_sector . entityName = 'blobs' AND SUBSTR ( orig_sector . entityId , 1 , 1 ) = ? ` ,
[ sector ]
) ;
2024-02-16 23:56:32 +02:00
for ( const ec of dependingEntityChanges ) {
putEntityChangeWithForcedChange ( ec ) ;
}
return dependingEntityChanges . length ;
}
function cleanupEntityChangesForMissingEntities ( entityName : string , entityPrimaryKey : string ) {
sql . execute ( `
2024-12-22 15:42:15 +02:00
DELETE
FROM entity_changes
WHERE
2024-02-16 23:56:32 +02:00
isErased = 0
2024-12-22 15:42:15 +02:00
AND entityName = '${entityName}'
2024-02-16 23:56:32 +02:00
AND entityId NOT IN ( SELECT $ { entityPrimaryKey } FROM $ { entityName } ) ` );
}
2025-01-09 18:07:02 +02:00
function fillEntityChanges ( entityName : string , entityPrimaryKey : string , condition = "" ) {
2024-02-16 23:56:32 +02:00
cleanupEntityChangesForMissingEntities ( entityName , entityPrimaryKey ) ;
sql . transactional ( ( ) = > {
2025-04-01 23:30:21 +03:00
const entityIds = sql . getColumn < string > ( /*sql*/ ` SELECT ${ entityPrimaryKey } FROM ${ entityName } ${ condition } ` ) ;
2024-02-16 23:56:32 +02:00
let createdCount = 0 ;
for ( const entityId of entityIds ) {
const existingRows = sql . getValue ( "SELECT COUNT(1) FROM entity_changes WHERE entityName = ? AND entityId = ?" , [ entityName , entityId ] ) ;
if ( existingRows !== 0 ) {
// we don't want to replace existing entities (which would effectively cause full resync)
continue ;
}
createdCount ++ ;
const ec : Partial < EntityChange > = {
entityName ,
entityId ,
isErased : false
} ;
2025-01-09 18:07:02 +02:00
if ( entityName === "blobs" ) {
2024-02-16 23:56:32 +02:00
const blob = sql . getRow < Blob > ( "SELECT blobId, content, utcDateModified FROM blobs WHERE blobId = ?" , [ entityId ] ) ;
ec . hash = blobService . calculateContentHash ( blob ) ;
ec . utcDateChanged = blob . utcDateModified ;
ec . isSynced = true ; // blobs are always synced
} else {
const entity = becca . getEntity ( entityName , entityId ) ;
if ( entity ) {
ec . hash = entity . generateHash ( ) ;
ec . utcDateChanged = entity . getUtcDateChanged ( ) || dateUtils . utcNowDateTime ( ) ;
2025-01-09 18:07:02 +02:00
ec . isSynced = entityName !== "options" || ! ! entity . isSynced ;
2024-02-16 23:56:32 +02:00
} else {
// entity might be null (not present in becca) when it's deleted
// this will produce different hash value than when entity is being deleted since then
// all normal hashed attributes are being used. Sync should recover from that, though.
ec . hash = "deleted" ;
ec . utcDateChanged = dateUtils . utcNowDateTime ( ) ;
ec . isSynced = true ; // deletable (the ones with isDeleted) entities are synced
}
}
putEntityChange ( ec as EntityChange ) ;
}
if ( createdCount > 0 ) {
log . info ( ` Created ${ createdCount } missing entity changes for entity ' ${ entityName } '. ` ) ;
}
} ) ;
}
function fillAllEntityChanges() {
sql . transactional ( ( ) = > {
sql . execute ( "DELETE FROM entity_changes WHERE isErased = 0" ) ;
fillEntityChanges ( "notes" , "noteId" ) ;
fillEntityChanges ( "branches" , "branchId" ) ;
fillEntityChanges ( "revisions" , "revisionId" ) ;
fillEntityChanges ( "attachments" , "attachmentId" ) ;
fillEntityChanges ( "blobs" , "blobId" ) ;
fillEntityChanges ( "attributes" , "attributeId" ) ;
fillEntityChanges ( "etapi_tokens" , "etapiTokenId" ) ;
2025-01-09 18:07:02 +02:00
fillEntityChanges ( "options" , "name" , "WHERE isSynced = 1" ) ;
2024-02-16 23:56:32 +02:00
} ) ;
}
function recalculateMaxEntityChangeId() {
maxEntityChangeId = sql . getValue < number > ( "SELECT COALESCE(MAX(id), 0) FROM entity_changes" ) ;
}
2024-07-18 21:47:30 +03:00
export default {
2024-02-16 23:56:32 +02:00
putNoteReorderingEntityChange ,
putEntityChangeForOtherInstances ,
putEntityChangeWithForcedChange ,
putEntityChange ,
putEntityChangeWithInstanceId ,
fillAllEntityChanges ,
addEntityChangesForSector ,
getMaxEntityChangeId : ( ) = > maxEntityChangeId ,
recalculateMaxEntityChangeId
} ;