Skip to content

Commit

Permalink
refactor: migrate stack repo to kysely (#15440)
Browse files Browse the repository at this point in the history
* wip

* wip: add tags

* wip

* sql

* pr feedback

* pr feedback

* ergonomic

* pr feedback

* pr feedback
  • Loading branch information
alextran1502 authored Jan 21, 2025
1 parent 887267b commit 318dd32
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 365 deletions.
5 changes: 3 additions & 2 deletions server/src/interfaces/stack.interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Updateable } from 'kysely';
import { StackEntity } from 'src/entities/stack.entity';

export const IStackRepository = 'IStackRepository';
Expand All @@ -10,8 +11,8 @@ export interface StackSearch {
export interface IStackRepository {
search(query: StackSearch): Promise<StackEntity[]>;
create(stack: { ownerId: string; assetIds: string[] }): Promise<StackEntity>;
update(stack: Pick<StackEntity, 'id'> & Partial<StackEntity>): Promise<StackEntity>;
update(id: string, entity: Updateable<StackEntity>): Promise<StackEntity>;
delete(id: string): Promise<void>;
deleteAll(ids: string[]): Promise<void>;
getById(id: string): Promise<StackEntity | null>;
getById(id: string): Promise<StackEntity | undefined>;
}
334 changes: 86 additions & 248 deletions server/src/queries/stack.repository.sql

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions server/src/repositories/access.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,7 @@ class ActivityAccess implements IActivityAccess {
.where('activity.id', 'in', [...activityIds])
.where('activity.userId', '=', userId)
.execute()
.then((activities) => {
console.log('activities', activities);
return new Set(activities.map((activity) => activity.id));
});
.then((activities) => new Set(activities.map((activity) => activity.id)));
}

@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
Expand Down
220 changes: 113 additions & 107 deletions server/src/repositories/stack.repository.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,113 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { ExpressionBuilder, Kysely, Updateable } from 'kysely';
import { jsonArrayFrom } from 'kysely/helpers/postgres';
import { InjectKysely } from 'nestjs-kysely';
import { DB } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { AssetEntity } from 'src/entities/asset.entity';
import { StackEntity } from 'src/entities/stack.entity';
import { IStackRepository, StackSearch } from 'src/interfaces/stack.interface';
import { DataSource, In, Repository } from 'typeorm';
import { asUuid } from 'src/utils/database';

const withAssets = (eb: ExpressionBuilder<DB, 'asset_stack'>, withTags = false) => {
return jsonArrayFrom(
eb
.selectFrom('assets')
.selectAll()
.$if(withTags, (eb) =>
eb.select((eb) =>
jsonArrayFrom(
eb
.selectFrom('tags')
.selectAll('tags')
.innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId')
.whereRef('tag_asset.assetsId', '=', 'assets.id'),
).as('tags'),
),
)
.where('assets.deletedAt', 'is', null)
.whereRef('assets.stackId', '=', 'asset_stack.id'),
).as('assets');
};

@Injectable()
export class StackRepository implements IStackRepository {
constructor(
@InjectDataSource() private dataSource: DataSource,
@InjectRepository(StackEntity) private repository: Repository<StackEntity>,
) {}
constructor(@InjectKysely() private db: Kysely<DB>) {}

@GenerateSql({ params: [{ ownerId: DummyValue.UUID }] })
search(query: StackSearch): Promise<StackEntity[]> {
return this.repository.find({
where: {
ownerId: query.ownerId,
primaryAssetId: query.primaryAssetId,
},
relations: {
assets: {
exifInfo: true,
},
},
});
return this.db
.selectFrom('asset_stack')
.selectAll('asset_stack')
.select(withAssets)
.where('asset_stack.ownerId', '=', query.ownerId)
.$if(!!query.primaryAssetId, (eb) => eb.where('asset_stack.primaryAssetId', '=', query.primaryAssetId!))
.execute() as unknown as Promise<StackEntity[]>;
}

async create(entity: { ownerId: string; assetIds: string[] }): Promise<StackEntity> {
return this.dataSource.manager.transaction(async (manager) => {
const stackRepository = manager.getRepository(StackEntity);

const stacks = await stackRepository.find({
where: {
ownerId: entity.ownerId,
primaryAssetId: In(entity.assetIds),
},
select: {
id: true,
assets: {
id: true,
},
},
relations: {
assets: {
exifInfo: true,
},
},
});
return this.db.transaction().execute(async (tx) => {
const stacks = await tx
.selectFrom('asset_stack')
.where('asset_stack.ownerId', '=', entity.ownerId)
.where('asset_stack.primaryAssetId', 'in', entity.assetIds)
.select('asset_stack.id')
.select((eb) =>
jsonArrayFrom(
eb
.selectFrom('assets')
.select('assets.id')
.whereRef('assets.stackId', '=', 'asset_stack.id')
.where('assets.deletedAt', 'is', null),
).as('assets'),
)
.execute();

const assetIds = new Set<string>(entity.assetIds);

// children
for (const stack of stacks) {
for (const asset of stack.assets) {
assetIds.add(asset.id);
if (stack.assets && stack.assets.length > 0) {
for (const asset of stack.assets) {
assetIds.add(asset.id);
}
}
}

if (stacks.length > 0) {
await stackRepository.delete({ id: In(stacks.map((stack) => stack.id)) });
await tx
.deleteFrom('asset_stack')
.where(
'id',
'in',
stacks.map((stack) => stack.id),
)
.execute();
}

const { id } = await stackRepository.save({
ownerId: entity.ownerId,
primaryAssetId: entity.assetIds[0],
assets: [...assetIds].map((id) => ({ id }) as AssetEntity),
});

return stackRepository.findOneOrFail({
where: {
id,
},
relations: {
assets: {
exifInfo: true,
},
},
});
const newRecord = await tx
.insertInto('asset_stack')
.values({
ownerId: entity.ownerId,
primaryAssetId: entity.assetIds[0],
})
.returning('id')
.executeTakeFirstOrThrow();

await tx
.updateTable('assets')
.set({
stackId: newRecord.id,
updatedAt: new Date(),
})
.where('id', 'in', [...assetIds])
.execute();

return tx
.selectFrom('asset_stack')
.selectAll('asset_stack')
.select(withAssets)
.where('id', '=', newRecord.id)
.executeTakeFirst() as unknown as Promise<StackEntity>;
});
}

Expand All @@ -91,12 +120,12 @@ export class StackRepository implements IStackRepository {

const assetIds = stack.assets.map(({ id }) => id);

await this.repository.delete(id);

// Update assets updatedAt
await this.dataSource.manager.update(AssetEntity, assetIds, {
updatedAt: new Date(),
});
await this.db.deleteFrom('asset_stack').where('id', '=', asUuid(id)).execute();
await this.db
.updateTable('assets')
.set({ stackId: null, updatedAt: new Date() })
.where('id', 'in', assetIds)
.execute();
}

async deleteAll(ids: string[]): Promise<void> {
Expand All @@ -110,54 +139,31 @@ export class StackRepository implements IStackRepository {
assetIds.push(...stack.assets.map(({ id }) => id));
}

await this.repository.delete(ids);

// Update assets updatedAt
await this.dataSource.manager.update(AssetEntity, assetIds, {
updatedAt: new Date(),
});
await this.db
.updateTable('assets')
.set({ updatedAt: new Date(), stackId: null })
.where('id', 'in', assetIds)
.where('stackId', 'in', ids)
.execute();
}

update(entity: Partial<StackEntity>) {
return this.save(entity);
update(id: string, entity: Updateable<StackEntity>): Promise<StackEntity> {
return this.db
.updateTable('asset_stack')
.set(entity)
.where('id', '=', asUuid(id))
.returningAll('asset_stack')
.returning((eb) => withAssets(eb, true))
.executeTakeFirstOrThrow() as unknown as Promise<StackEntity>;
}

@GenerateSql({ params: [DummyValue.UUID] })
async getById(id: string): Promise<StackEntity | null> {
return this.repository.findOne({
where: {
id,
},
relations: {
assets: {
exifInfo: true,
tags: true,
},
},
order: {
assets: {
fileCreatedAt: 'ASC',
},
},
});
}

private async save(entity: Partial<StackEntity>) {
const { id } = await this.repository.save(entity);
return this.repository.findOneOrFail({
where: {
id,
},
relations: {
assets: {
exifInfo: true,
},
},
order: {
assets: {
fileCreatedAt: 'ASC',
},
},
});
getById(id: string): Promise<StackEntity | undefined> {
return this.db
.selectFrom('asset_stack')
.selectAll()
.select((eb) => withAssets(eb, true))
.where('id', '=', asUuid(id))
.executeTakeFirst() as Promise<StackEntity | undefined>;
}
}
2 changes: 1 addition & 1 deletion server/src/services/asset.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ describe(AssetService.name, () => {

await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });

expect(stackMock.update).toHaveBeenCalledWith({
expect(stackMock.update).toHaveBeenCalledWith('stack-1', {
id: 'stack-1',
primaryAssetId: 'stack-child-asset-1',
});
Expand Down
2 changes: 1 addition & 1 deletion server/src/services/asset.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export class AssetService extends BaseService {
const stackAssetIds = asset.stack.assets.map((a) => a.id);
if (stackAssetIds.length > 2) {
const newPrimaryAssetId = stackAssetIds.find((a) => a !== id)!;
await this.stackRepository.update({
await this.stackRepository.update(asset.stack.id, {
id: asset.stack.id,
primaryAssetId: newPrimaryAssetId,
});
Expand Down
5 changes: 4 additions & 1 deletion server/src/services/stack.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,10 @@ describe(StackService.name, () => {
await sut.update(authStub.admin, 'stack-id', { primaryAssetId: assetStub.image1.id });

expect(stackMock.getById).toHaveBeenCalledWith('stack-id');
expect(stackMock.update).toHaveBeenCalledWith({ id: 'stack-id', primaryAssetId: assetStub.image1.id });
expect(stackMock.update).toHaveBeenCalledWith('stack-id', {
id: 'stack-id',
primaryAssetId: assetStub.image1.id,
});
expect(eventMock.emit).toHaveBeenCalledWith('stack.update', {
stackId: 'stack-id',
userId: authStub.admin.user.id,
Expand Down
2 changes: 1 addition & 1 deletion server/src/services/stack.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class StackService extends BaseService {
throw new BadRequestException('Primary asset must be in the stack');
}

const updatedStack = await this.stackRepository.update({ id, primaryAssetId: dto.primaryAssetId });
const updatedStack = await this.stackRepository.update(id, { id, primaryAssetId: dto.primaryAssetId });

await this.eventRepository.emit('stack.update', { stackId: id, userId: auth.user.id });

Expand Down

0 comments on commit 318dd32

Please sign in to comment.