fix: missing deletedAt and isVisible columns on mobile (#26414)

* feat: SyncAssetV2

* feat: mobile sync handling

* feat: request correct sync object based on server version

* fix: mobile queries

* chore: sync sql

* fix: test

* chore: switch to mapper

* fix: sql sync
This commit is contained in:
Brandon Wees
2026-02-23 08:50:54 -06:00
committed by GitHub
parent a07d7b0c82
commit e633bc3f24
28 changed files with 9803 additions and 92 deletions

View File

@@ -97,3 +97,134 @@ describe(SyncEntityType.AssetFaceV1, () => {
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV1]);
});
});
describe(SyncEntityType.AssetFaceV2, () => {
it('should detect and sync the first asset face', async () => {
const { auth, ctx } = await setup();
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
const { person } = await ctx.newPerson({ ownerId: auth.user.id });
const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personId: person.id });
const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]);
expect(response).toEqual([
{
ack: expect.any(String),
data: expect.objectContaining({
id: assetFace.id,
assetId: asset.id,
personId: person.id,
imageWidth: assetFace.imageWidth,
imageHeight: assetFace.imageHeight,
boundingBoxX1: assetFace.boundingBoxX1,
boundingBoxY1: assetFace.boundingBoxY1,
boundingBoxX2: assetFace.boundingBoxX2,
boundingBoxY2: assetFace.boundingBoxY2,
sourceType: assetFace.sourceType,
}),
type: 'AssetFaceV2',
},
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.syncAckAll(auth, response);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]);
});
it('should detect and sync a deleted asset face', async () => {
const { auth, ctx } = await setup();
const personRepo = ctx.get(PersonRepository);
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
const { assetFace } = await ctx.newAssetFace({ assetId: asset.id });
await personRepo.deleteAssetFace(assetFace.id);
const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]);
expect(response).toEqual([
{
ack: expect.any(String),
data: {
assetFaceId: assetFace.id,
},
type: 'AssetFaceDeleteV1',
},
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.syncAckAll(auth, response);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]);
});
it('should not sync an asset face or asset face delete for an unrelated user', async () => {
const { auth, ctx } = await setup();
const personRepo = ctx.get(PersonRepository);
const { user: user2 } = await ctx.newUser();
const { session } = await ctx.newSession({ userId: user2.id });
const { asset } = await ctx.newAsset({ ownerId: user2.id });
const { assetFace } = await ctx.newAssetFace({ assetId: asset.id });
const auth2 = factory.auth({ session, user: user2 });
expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV2])).toEqual([
expect.objectContaining({ type: SyncEntityType.AssetFaceV2 }),
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]);
await personRepo.deleteAssetFace(assetFace.id);
expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV2])).toEqual([
expect.objectContaining({ type: SyncEntityType.AssetFaceDeleteV1 }),
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]);
});
it('should contain the deletedAt and isVisible fields in AssetFaceV2', async () => {
const { auth, ctx } = await setup();
const personRepo = ctx.get(PersonRepository);
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
const { person } = await ctx.newPerson({ ownerId: auth.user.id });
const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personId: person.id });
let response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]);
expect(response).toEqual([
{
ack: expect.any(String),
data: expect.objectContaining({
id: assetFace.id,
assetId: asset.id,
personId: person.id,
imageWidth: assetFace.imageWidth,
imageHeight: assetFace.imageHeight,
boundingBoxX1: assetFace.boundingBoxX1,
boundingBoxY1: assetFace.boundingBoxY1,
boundingBoxX2: assetFace.boundingBoxX2,
boundingBoxY2: assetFace.boundingBoxY2,
sourceType: assetFace.sourceType,
deletedAt: null,
isVisible: true,
}),
type: 'AssetFaceV2',
},
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.syncAckAll(auth, response);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]);
await personRepo.deleteAssetFace(assetFace.id);
response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]);
expect(response).toEqual([
{
ack: expect.any(String),
data: {
assetFaceId: assetFace.id,
},
type: 'AssetFaceDeleteV1',
},
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.syncAckAll(auth, response);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]);
});
});