mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
refactor(sync): remove tombstone system
Tombstones were used for tracking deleted entities but are no longer needed with the operation log architecture. This removes: - Tombstone table from Prisma schema - Tombstone-related methods from SyncService - Tombstone mocks and tests from all test files - Database migration to drop tombstones table The operation log now handles deletions through DEL operations, making the separate tombstone tracking redundant.
This commit is contained in:
parent
805327731b
commit
a52b7716aa
15 changed files with 5 additions and 178 deletions
|
|
@ -0,0 +1,2 @@
|
|||
-- DropTable
|
||||
DROP TABLE IF EXISTS "tombstones";
|
||||
|
|
@ -29,7 +29,6 @@ model User {
|
|||
operations Operation[]
|
||||
syncState UserSyncState?
|
||||
devices SyncDevice[]
|
||||
tombstones Tombstone[]
|
||||
|
||||
@@index([verificationToken])
|
||||
@@map("users")
|
||||
|
|
@ -89,17 +88,3 @@ model SyncDevice {
|
|||
@@map("sync_devices")
|
||||
}
|
||||
|
||||
model Tombstone {
|
||||
userId Int @map("user_id")
|
||||
entityType String @map("entity_type")
|
||||
entityId String @map("entity_id")
|
||||
deletedAt BigInt @map("deleted_at")
|
||||
deletedByOpId String @map("deleted_by_op_id")
|
||||
expiresAt BigInt @map("expires_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([userId, entityType, entityId])
|
||||
@@index([expiresAt])
|
||||
@@map("tombstones")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,11 +58,8 @@ async function main() {
|
|||
prisma.operation.deleteMany(),
|
||||
prisma.userSyncState.deleteMany(),
|
||||
prisma.syncDevice.deleteMany(),
|
||||
prisma.tombstone.deleteMany(),
|
||||
]);
|
||||
Logger.info(
|
||||
'Deleted all rows from sync tables (operations, sync_state, devices, tombstones)',
|
||||
);
|
||||
Logger.info('Deleted all rows from sync tables (operations, sync_state, devices)');
|
||||
} catch (err) {
|
||||
Logger.error('Failed to clear database tables:', err);
|
||||
process.exit(1);
|
||||
|
|
@ -123,7 +120,6 @@ async function main() {
|
|||
prisma.operation.deleteMany({ where: { userId: user.id } }),
|
||||
prisma.userSyncState.deleteMany({ where: { userId: user.id } }),
|
||||
prisma.syncDevice.deleteMany({ where: { userId: user.id } }),
|
||||
prisma.tombstone.deleteMany({ where: { userId: user.id } }),
|
||||
]);
|
||||
Logger.info(`Deleted sync data for user ${user.id}`);
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -10,13 +10,7 @@ export const prisma = new PrismaClient({
|
|||
});
|
||||
|
||||
// Re-export types for convenience
|
||||
export type {
|
||||
User,
|
||||
Operation,
|
||||
UserSyncState,
|
||||
SyncDevice,
|
||||
Tombstone,
|
||||
} from '@prisma/client';
|
||||
export type { User, Operation, UserSyncState, SyncDevice } from '@prisma/client';
|
||||
|
||||
// Helper to disconnect on shutdown
|
||||
export const disconnectDb = async (): Promise<void> => {
|
||||
|
|
|
|||
|
|
@ -695,7 +695,6 @@ export const syncRoutes = async (fastify: FastifyInstance): Promise<void> => {
|
|||
const response: SyncStatusResponse = {
|
||||
latestSeq,
|
||||
devicesOnline,
|
||||
pendingOps: 0, // Deprecated: ACK-based tracking removed
|
||||
snapshotAge,
|
||||
storageUsedBytes: storageInfo.storageUsedBytes,
|
||||
storageQuotaBytes: storageInfo.storageQuotaBytes,
|
||||
|
|
|
|||
|
|
@ -388,11 +388,6 @@ export class SyncService {
|
|||
},
|
||||
});
|
||||
|
||||
// Create tombstone for delete operations
|
||||
if (op.opType === 'DEL' && op.entityId) {
|
||||
await this.createTombstoneSync(userId, op.entityType, op.entityId, op.id, tx);
|
||||
}
|
||||
|
||||
return {
|
||||
opId: op.id,
|
||||
accepted: true,
|
||||
|
|
@ -427,42 +422,6 @@ export class SyncService {
|
|||
}
|
||||
}
|
||||
|
||||
private async createTombstoneSync(
|
||||
userId: number,
|
||||
entityType: string,
|
||||
entityId: string,
|
||||
deletedByOpId: string,
|
||||
tx: Prisma.TransactionClient,
|
||||
): Promise<void> {
|
||||
const now = Date.now();
|
||||
const expiresAt = now + this.config.tombstoneRetentionMs;
|
||||
|
||||
await tx.tombstone.upsert({
|
||||
where: {
|
||||
// Prisma composite key naming uses underscores; allow it here
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
userId_entityType_entityId: {
|
||||
userId,
|
||||
entityType,
|
||||
entityId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
userId,
|
||||
entityType,
|
||||
entityId,
|
||||
deletedAt: BigInt(now),
|
||||
deletedByOpId,
|
||||
expiresAt: BigInt(expiresAt),
|
||||
},
|
||||
update: {
|
||||
deletedAt: BigInt(now),
|
||||
deletedByOpId,
|
||||
expiresAt: BigInt(expiresAt),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// === Download Operations ===
|
||||
// Delegated to OperationDownloadService
|
||||
|
||||
|
|
@ -609,15 +568,6 @@ export class SyncService {
|
|||
|
||||
// === Cleanup ===
|
||||
|
||||
async deleteExpiredTombstones(): Promise<number> {
|
||||
const result = await prisma.tombstone.deleteMany({
|
||||
where: {
|
||||
expiresAt: { lt: BigInt(Date.now()) },
|
||||
},
|
||||
});
|
||||
return result.count;
|
||||
}
|
||||
|
||||
async deleteOldSyncedOpsForAllUsers(
|
||||
cutoffTime: number,
|
||||
): Promise<{ totalDeleted: number; affectedUserIds: number[] }> {
|
||||
|
|
@ -860,16 +810,13 @@ export class SyncService {
|
|||
|
||||
/**
|
||||
* Delete ALL sync data for a user. Used for encryption password changes.
|
||||
* Deletes operations, tombstones, devices, and resets sync state.
|
||||
* Deletes operations, devices, and resets sync state.
|
||||
*/
|
||||
async deleteAllUserData(userId: number): Promise<void> {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Delete all operations
|
||||
await tx.operation.deleteMany({ where: { userId } });
|
||||
|
||||
// Delete all tombstones
|
||||
await tx.tombstone.deleteMany({ where: { userId } });
|
||||
|
||||
// Delete all devices
|
||||
await tx.syncDevice.deleteMany({ where: { userId } });
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ export const testRoutes = async (fastify: FastifyInstance): Promise<void> => {
|
|||
prisma.operation.deleteMany({ where: { userId } }),
|
||||
prisma.syncDevice.deleteMany({ where: { userId } }),
|
||||
prisma.userSyncState.deleteMany({ where: { userId } }),
|
||||
prisma.tombstone.deleteMany({ where: { userId } }),
|
||||
]);
|
||||
} else {
|
||||
// Create user with isVerified=1 (skip email verification)
|
||||
|
|
@ -134,7 +133,6 @@ export const testRoutes = async (fastify: FastifyInstance): Promise<void> => {
|
|||
prisma.operation.deleteMany(),
|
||||
prisma.syncDevice.deleteMany(),
|
||||
prisma.userSyncState.deleteMany(),
|
||||
prisma.tombstone.deleteMany(),
|
||||
prisma.user.deleteMany(),
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -108,19 +108,6 @@ vi.mock('../src/db', async () => {
|
|||
return result;
|
||||
}),
|
||||
},
|
||||
tombstone: {
|
||||
upsert: vi.fn().mockImplementation(async (args: any) => {
|
||||
const key = `${args.where.userId_entityType_entityId.userId}:${args.where.userId_entityType_entityId.entityType}:${args.where.userId_entityType_entityId.entityId}`;
|
||||
const result = {
|
||||
...args.create,
|
||||
userId: args.where.userId_entityType_entityId.userId,
|
||||
entityType: args.where.userId_entityType_entityId.entityType,
|
||||
entityId: args.where.userId_entityType_entityId.entityId,
|
||||
};
|
||||
state.tombstones.set(key, result);
|
||||
return result;
|
||||
}),
|
||||
},
|
||||
user: {
|
||||
findUnique: vi.fn().mockImplementation(async (args: any) => {
|
||||
return state.users.get(args.where.id) || null;
|
||||
|
|
|
|||
|
|
@ -120,19 +120,6 @@ vi.mock('../src/db', async () => {
|
|||
return result;
|
||||
}),
|
||||
},
|
||||
tombstone: {
|
||||
upsert: vi.fn().mockImplementation(async (args: any) => {
|
||||
const key = `${args.where.userId_entityType_entityId.userId}:${args.where.userId_entityType_entityId.entityType}:${args.where.userId_entityType_entityId.entityId}`;
|
||||
const result = {
|
||||
...args.create,
|
||||
userId: args.where.userId_entityType_entityId.userId,
|
||||
entityType: args.where.userId_entityType_entityId.entityType,
|
||||
entityId: args.where.userId_entityType_entityId.entityId,
|
||||
};
|
||||
state.tombstones.set(key, result);
|
||||
return result;
|
||||
}),
|
||||
},
|
||||
user: {
|
||||
findUnique: vi.fn().mockImplementation(async (args: any) => {
|
||||
return state.users.get(args.where.id) || null;
|
||||
|
|
@ -186,10 +173,6 @@ vi.mock('../src/db', async () => {
|
|||
syncDevice: {
|
||||
findMany: vi.fn().mockImplementation(async () => []),
|
||||
},
|
||||
tombstone: {
|
||||
findMany: vi.fn().mockImplementation(async () => []),
|
||||
deleteMany: vi.fn().mockImplementation(async () => ({ count: 0 })),
|
||||
},
|
||||
user: {
|
||||
findUnique: vi.fn().mockImplementation(async (args: any) => {
|
||||
return state.users.get(args.where.id) || null;
|
||||
|
|
|
|||
|
|
@ -884,18 +884,6 @@ describe('Multi-Client Sync Integration', () => {
|
|||
expect(ops[0].op.opType).toBe('CRT');
|
||||
expect(ops[1].op.opType).toBe('DEL');
|
||||
});
|
||||
|
||||
it('should track tombstones for deleted entities', async () => {
|
||||
const client = new SimulatedClient(app, userId);
|
||||
|
||||
client.createOp('CRT', 'TASK', 'task-1', { title: 'Will Delete' });
|
||||
client.createOp('DEL', 'TASK', 'task-1', null);
|
||||
await client.upload();
|
||||
|
||||
const service = getSyncService();
|
||||
const isTombstoned = service.isTombstoned(userId, 'TASK', 'task-1');
|
||||
expect(isTombstoned).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Cases', () => {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ interface TestData {
|
|||
operations: Map<string, any>;
|
||||
syncDevices: Map<string, any>;
|
||||
userSyncStates: Map<number, any>;
|
||||
tombstones: Map<string, any>;
|
||||
}
|
||||
|
||||
let testData: TestData = {
|
||||
|
|
@ -20,7 +19,6 @@ let testData: TestData = {
|
|||
operations: new Map(),
|
||||
syncDevices: new Map(),
|
||||
userSyncStates: new Map(),
|
||||
tombstones: new Map(),
|
||||
};
|
||||
|
||||
let serverSeqCounter = 0;
|
||||
|
|
@ -44,15 +42,6 @@ const createMockDb = () => {
|
|||
if (sql.includes('UPDATE sync_devices SET last_seen_at')) {
|
||||
return { changes: 1 };
|
||||
}
|
||||
if (sql.includes('INSERT INTO tombstones')) {
|
||||
const [userId, entityType, entityId] = args;
|
||||
testData.tombstones.set(`${userId}:${entityType}:${entityId}`, {
|
||||
userId,
|
||||
entityType,
|
||||
entityId,
|
||||
});
|
||||
return { changes: 1 };
|
||||
}
|
||||
return { changes: 0 };
|
||||
},
|
||||
get: (...args: any[]) => {
|
||||
|
|
@ -86,7 +75,6 @@ export const initDb = (dataPath: string, inMemory: boolean = false) => {
|
|||
operations: new Map(),
|
||||
syncDevices: new Map(),
|
||||
userSyncStates: new Map(),
|
||||
tombstones: new Map(),
|
||||
};
|
||||
serverSeqCounter = 0;
|
||||
mockDb = createMockDb();
|
||||
|
|
@ -173,18 +161,6 @@ vi.mock('../src/db', () => {
|
|||
count: vi.fn().mockResolvedValue(1),
|
||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
},
|
||||
tombstone: {
|
||||
upsert: vi.fn().mockImplementation(async (args: any) => {
|
||||
const key = `${args.where.userId_entityType_entityId.userId}:${args.where.userId_entityType_entityId.entityType}:${args.where.userId_entityType_entityId.entityId}`;
|
||||
testData.tombstones.set(key, args.create);
|
||||
return args.create;
|
||||
}),
|
||||
findUnique: vi.fn().mockImplementation(async (args: any) => {
|
||||
const key = `${args.where.userId_entityType_entityId.userId}:${args.where.userId_entityType_entityId.entityType}:${args.where.userId_entityType_entityId.entityId}`;
|
||||
return testData.tombstones.get(key) || null;
|
||||
}),
|
||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
},
|
||||
user: {
|
||||
findUnique: vi.fn().mockImplementation(async (args: any) => {
|
||||
return testData.users.get(args.where.id) || null;
|
||||
|
|
@ -217,11 +193,6 @@ vi.mock('../src/db', () => {
|
|||
count: vi.fn().mockResolvedValue(1),
|
||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
},
|
||||
tombstone: {
|
||||
upsert: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
},
|
||||
user: {
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
|
|
@ -238,7 +209,6 @@ vi.mock('../src/db', () => {
|
|||
operations: new Map(),
|
||||
syncDevices: new Map(),
|
||||
userSyncStates: new Map(),
|
||||
tombstones: new Map(),
|
||||
};
|
||||
serverSeqCounter = 0;
|
||||
mockDb = createMockDb();
|
||||
|
|
@ -264,7 +234,6 @@ beforeEach(() => {
|
|||
operations: new Map(),
|
||||
syncDevices: new Map(),
|
||||
userSyncStates: new Map(),
|
||||
tombstones: new Map(),
|
||||
};
|
||||
serverSeqCounter = 0;
|
||||
vi.clearAllMocks();
|
||||
|
|
|
|||
|
|
@ -106,10 +106,6 @@ vi.mock('../src/db', () => {
|
|||
upsert: vi.fn().mockResolvedValue({}),
|
||||
count: vi.fn().mockResolvedValue(1),
|
||||
},
|
||||
tombstone: {
|
||||
upsert: vi.fn().mockResolvedValue({}),
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
};
|
||||
return callback(tx);
|
||||
}),
|
||||
|
|
@ -190,11 +186,6 @@ vi.mock('../src/db', () => {
|
|||
count: vi.fn().mockResolvedValue(1),
|
||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
},
|
||||
tombstone: {
|
||||
upsert: vi.fn().mockResolvedValue({}),
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
},
|
||||
user: {
|
||||
findUnique: vi.fn().mockImplementation(async (args: any) => {
|
||||
return testUsers.get(args.where.id) || null;
|
||||
|
|
|
|||
|
|
@ -84,10 +84,6 @@ vi.mock('../src/db', () => {
|
|||
upsert: vi.fn().mockResolvedValue({}),
|
||||
count: vi.fn().mockResolvedValue(1),
|
||||
},
|
||||
tombstone: {
|
||||
upsert: vi.fn().mockResolvedValue({}),
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
};
|
||||
return callback(tx);
|
||||
}),
|
||||
|
|
@ -126,11 +122,6 @@ vi.mock('../src/db', () => {
|
|||
count: vi.fn().mockResolvedValue(1),
|
||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
},
|
||||
tombstone: {
|
||||
upsert: vi.fn().mockResolvedValue({}),
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
},
|
||||
user: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
id: 1,
|
||||
|
|
|
|||
|
|
@ -464,7 +464,6 @@ describe('Sync Routes', () => {
|
|||
const body = response.json();
|
||||
expect(body.latestSeq).toBeDefined();
|
||||
expect(body.devicesOnline).toBeDefined();
|
||||
expect(body.pendingOps).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return 401 without authorization', async () => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
export const testState = {
|
||||
operations: new Map<string, any>(),
|
||||
syncDevices: new Map<string, any>(),
|
||||
tombstones: new Map<string, any>(),
|
||||
userSyncStates: new Map<number, any>(),
|
||||
users: new Map<number, any>(),
|
||||
serverSeqCounter: 0,
|
||||
|
|
@ -16,7 +15,6 @@ export const testState = {
|
|||
export function resetTestState(): void {
|
||||
testState.operations = new Map();
|
||||
testState.syncDevices = new Map();
|
||||
testState.tombstones = new Map();
|
||||
testState.userSyncStates = new Map();
|
||||
testState.users = new Map();
|
||||
testState.serverSeqCounter = 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue