diff --git a/src/datasources/db/cargobikeAPI.ts b/src/datasources/db/cargobikeAPI.ts index 4fbbc44..3b860ee 100644 --- a/src/datasources/db/cargobikeAPI.ts +++ b/src/datasources/db/cargobikeAPI.ts @@ -26,7 +26,7 @@ import { Equipment } from '../../model/Equipment'; import { Engagement } from '../../model/Engagement'; import { Provider } from '../../model/Provider'; import { TimeFrame } from '../../model/TimeFrame'; -import { ActionLogger, deleteEntity, getAllEntity, LockUtils } from './utils'; +import { ActionLogger, DBUtils, LockUtils } from './utils'; import { EquipmentType } from '../../model/EquipmentType'; import { BikeEventType } from '../../model/BikeEventType'; import { UserInputError } from 'apollo-server-express'; @@ -43,7 +43,7 @@ export class CargoBikeAPI extends DataSource { } async getCargoBikes (offset?: number, limit?: number) { - return await getAllEntity(this.connection, CargoBike, 'cb', offset, limit); + return await DBUtils.getAllEntity(this.connection, CargoBike, 'cb', offset, limit); } /** @@ -192,11 +192,11 @@ export class CargoBikeAPI extends DataSource { } async deleteBikeEventType (id: number, userId: number) { - return await deleteEntity(this.connection, BikeEventType, 'bet', id, userId); + return await DBUtils.deleteEntity(this.connection, BikeEventType, 'bet', id, userId); } async deleteBikeEvent (id: number, userId: number) { - return await deleteEntity(this.connection, BikeEvent, 'be', id, userId); + return await DBUtils.deleteEntity(this.connection, BikeEvent, 'be', id, userId); } async cargoBikeByEventId (id: number) { @@ -270,11 +270,11 @@ export class CargoBikeAPI extends DataSource { } async bikeEventTypes (offset?: number, limit?: number) { - return await getAllEntity(this.connection, BikeEventType, 'bet', offset, limit); + return await DBUtils.getAllEntity(this.connection, BikeEventType, 'bet', offset, limit); } async bikeEvents (offset?: number, limit?: number) { - return await getAllEntity(this.connection, BikeEvent, 'be', offset, limit); + return await DBUtils.getAllEntity(this.connection, BikeEvent, 'be', offset, limit); } async bikeEventTypeById (id: number) { @@ -408,7 +408,7 @@ export class CargoBikeAPI extends DataSource { } async deleteEquipment (id: number, userId: number) { - return await deleteEntity(this.connection, Equipment, 'e', id, userId); + return await DBUtils.deleteEntity(this.connection, Equipment, 'e', id, userId); } async getEquipment (offset: number, limit: number) { @@ -462,7 +462,7 @@ export class CargoBikeAPI extends DataSource { } async deleteEquipmentType (id:number, userId: number) { - return await deleteEntity(this.connection, EquipmentType, 'et', id, userId); + return await DBUtils.deleteEntity(this.connection, EquipmentType, 'et', id, userId); } async equipmentTypeById (id: number) { diff --git a/src/datasources/db/contactinformationAPI.ts b/src/datasources/db/contactinformationAPI.ts index a2312db..e3e9a28 100644 --- a/src/datasources/db/contactinformationAPI.ts +++ b/src/datasources/db/contactinformationAPI.ts @@ -21,7 +21,7 @@ import { DataSource } from 'apollo-datasource'; import { Connection, EntityManager, getConnection } from 'typeorm'; import { ContactInformation } from '../../model/ContactInformation'; import { Person } from '../../model/Person'; -import { ActionLogger, deleteEntity, getAllEntity, LockUtils } from './utils'; +import { ActionLogger, DBUtils, LockUtils } from './utils'; import { GraphQLError } from 'graphql'; import { LendingStation } from '../../model/LendingStation'; @@ -33,7 +33,7 @@ export class ContactInformationAPI extends DataSource { } async contactInformation (offset?: number, limit?: number) { - return await getAllEntity(this.connection, ContactInformation, 'ci', offset, limit); + return await DBUtils.getAllEntity(this.connection, ContactInformation, 'ci', offset, limit); } async contactInformationById (id: number) { @@ -83,11 +83,11 @@ export class ContactInformationAPI extends DataSource { } async deletePerson (id: number, userId: number) { - return await deleteEntity(this.connection, Person, 'p', id, userId); + return await DBUtils.deleteEntity(this.connection, Person, 'p', id, userId); } async persons (offset?: number, limit?: number) { - return await getAllEntity(this.connection, Person, 'p', offset, limit); + return await DBUtils.getAllEntity(this.connection, Person, 'p', offset, limit); } async personById (id: number) { @@ -161,7 +161,7 @@ export class ContactInformationAPI extends DataSource { } async deleteContactInformation (id: number, userId: number) { - return await deleteEntity(this.connection, ContactInformation, 'ci', id, userId); + return await DBUtils.deleteEntity(this.connection, ContactInformation, 'ci', id, userId); } async contactInformationByPersonId (id: number) { diff --git a/src/datasources/db/lendingstationAPI.ts b/src/datasources/db/lendingstationAPI.ts index 9b3e53f..97f2b6f 100644 --- a/src/datasources/db/lendingstationAPI.ts +++ b/src/datasources/db/lendingstationAPI.ts @@ -24,7 +24,7 @@ import { Connection, EntityManager, getConnection } from 'typeorm'; import { CargoBike } from '../../model/CargoBike'; import { LendingStation } from '../../model/LendingStation'; import { TimeFrame } from '../../model/TimeFrame'; -import { ActionLogger, deleteEntity, genDateRange, getAllEntity, LockUtils } from './utils'; +import { ActionLogger, genDateRange, DBUtils, LockUtils } from './utils'; export class LendingStationAPI extends DataSource { connection : Connection @@ -45,7 +45,7 @@ export class LendingStationAPI extends DataSource { * get all lendingStations */ async lendingStations (offset?: number, limit?: number) { - return await getAllEntity(this.connection, LendingStation, 'ls', offset, limit); + return await DBUtils.getAllEntity(this.connection, LendingStation, 'ls', offset, limit); } /** @@ -74,7 +74,7 @@ export class LendingStationAPI extends DataSource { } async timeFrames (offset?: number, limit?: number) { - return await getAllEntity(this.connection, TimeFrame, 'tf', offset, limit); + return await DBUtils.getAllEntity(this.connection, TimeFrame, 'tf', offset, limit); } async timeFramesByCargoBikeId (id: number) { @@ -179,7 +179,7 @@ export class LendingStationAPI extends DataSource { } async deleteLendingStationById (id: number, userId: number) { - return await deleteEntity(this.connection, LendingStation, 'ls', id, userId); + return await DBUtils.deleteEntity(this.connection, LendingStation, 'ls', id, userId); } async createTimeFrame (timeFrame: any) { @@ -257,6 +257,6 @@ export class LendingStationAPI extends DataSource { } async deleteTimeFrame (id: number, userId: number) { - return await deleteEntity(this.connection, TimeFrame, 'tf', id, userId); + return await DBUtils.deleteEntity(this.connection, TimeFrame, 'tf', id, userId); } } diff --git a/src/datasources/db/participantAPI.ts b/src/datasources/db/participantAPI.ts index d492e55..d480f9e 100644 --- a/src/datasources/db/participantAPI.ts +++ b/src/datasources/db/participantAPI.ts @@ -23,7 +23,7 @@ import { ContactInformation } from '../../model/ContactInformation'; import { Engagement } from '../../model/Engagement'; import { Participant } from '../../model/Participant'; import { EngagementType } from '../../model/EngagementType'; -import { ActionLogger, deleteEntity, genDateRange, getAllEntity, LockUtils } from './utils'; +import { ActionLogger, DBUtils, genDateRange, LockUtils } from './utils'; import { UserInputError } from 'apollo-server-express'; import { GraphQLError } from 'graphql'; @@ -43,7 +43,7 @@ export class ParticipantAPI extends DataSource { } async getParticipants (offset?: number, limit?: number) { - return await getAllEntity(this.connection, Participant, 'p', offset, limit); + return await DBUtils.getAllEntity(this.connection, Participant, 'p', offset, limit); } async participantByEngagementId (id: number) { @@ -110,7 +110,7 @@ export class ParticipantAPI extends DataSource { } async engagements (offset?: number, limit?: number) { - return await getAllEntity(this.connection, Engagement, 'e', offset, limit); + return await DBUtils.getAllEntity(this.connection, Engagement, 'e', offset, limit); } async engagementById (id: number) { @@ -130,7 +130,7 @@ export class ParticipantAPI extends DataSource { } async engagementTypes (offset?: number, limit?: number) { - return await getAllEntity(this.connection, Engagement, 'e', offset, limit); + return await DBUtils.getAllEntity(this.connection, Engagement, 'e', offset, limit); } async engagementTypeByEngagementId (id: number) { @@ -223,7 +223,7 @@ export class ParticipantAPI extends DataSource { } async deleteParticipant (id: number, userId: number) { - return await deleteEntity(this.connection, Participant, 'p', id, userId); + return await DBUtils.deleteEntity(this.connection, Participant, 'p', id, userId); } async createEngagement (engagement: any) { @@ -292,7 +292,7 @@ export class ParticipantAPI extends DataSource { } async deleteEngagement (id: number, userId: number) { - return await deleteEntity(this.connection, Engagement, 'e', id, userId); + return await DBUtils.deleteEntity(this.connection, Engagement, 'e', id, userId); } async createEngagementType (engagementType: any) { @@ -334,6 +334,6 @@ export class ParticipantAPI extends DataSource { } async deleteEngagementType (id: number, userId: number) { - return await deleteEntity(this.connection, EngagementType, 'et', id, userId); + return await DBUtils.deleteEntity(this.connection, EngagementType, 'et', id, userId); } } diff --git a/src/datasources/db/providerAPI.ts b/src/datasources/db/providerAPI.ts index dd9511c..56df367 100644 --- a/src/datasources/db/providerAPI.ts +++ b/src/datasources/db/providerAPI.ts @@ -24,7 +24,7 @@ import { Organisation } from '../../model/Organisation'; import { UserInputError } from 'apollo-server-express'; import { CargoBike } from '../../model/CargoBike'; import { LendingStation } from '../../model/LendingStation'; -import { ActionLogger, deleteEntity, getAllEntity, LockUtils } from './utils'; +import { ActionLogger, DBUtils, LockUtils } from './utils'; import { GraphQLError } from 'graphql'; export class ProviderAPI extends DataSource { @@ -43,7 +43,7 @@ export class ProviderAPI extends DataSource { } async provider (offset?: number, limit?: number) { - return await getAllEntity(this.connection, Provider, 'p', offset, limit); + return await DBUtils.getAllEntity(this.connection, Provider, 'p', offset, limit); } async providerByOrganisationId (id: number) { @@ -71,7 +71,7 @@ export class ProviderAPI extends DataSource { } async organisations (offset?: number, limit?: number) { - return await getAllEntity(this.connection, Organisation, 'o', offset, limit); + return await DBUtils.getAllEntity(this.connection, Organisation, 'o', offset, limit); } async organisationById (id: number) { @@ -170,7 +170,7 @@ export class ProviderAPI extends DataSource { } async deleteProvider (id: number, userId: number) { - return await deleteEntity(this.connection, Provider, 'p', id, userId); + return await DBUtils.deleteEntity(this.connection, Provider, 'p', id, userId); } async createOrganisation (organisation: any) { @@ -213,6 +213,6 @@ export class ProviderAPI extends DataSource { } async deleteOrganisation (id: number, userId: number) { - return await deleteEntity(this.connection, Organisation, 'o', id, userId); + return await DBUtils.deleteEntity(this.connection, Organisation, 'o', id, userId); } } diff --git a/src/datasources/db/utils.ts b/src/datasources/db/utils.ts index 43e3554..962297f 100644 --- a/src/datasources/db/utils.ts +++ b/src/datasources/db/utils.ts @@ -36,52 +36,93 @@ export function genDateRange (struct: any) { } /** - * Can be used in resolvers to specify if entry is locked by other user. + * Can be used in resolvers to specify, if entry is locked by other user. * Returns true if locked by other user. * @param parent - * @param dataSources - * @param req user request + * @param req */ export function isLocked (parent: any, { req }: { req: any }) { return req.userId !== parent.lockedBy && new Date() <= new Date(parent.lockedUntil); } +/** + * Can be used in resolvers to specify, if entry is locked by the current user. + * @param parent + * @param req + */ export function isLockedByMe (parent: any, { req }: { req: any }) { return req.userId === parent.lockedBy && new Date() <= new Date(parent.lockedUntil); } -export async function deleteEntity (connection: Connection, target: ObjectType, alias: string, id: number, userId: number): Promise { - return await connection.transaction(async (entityManger: EntityManager) => { - if (await LockUtils.isLocked(entityManger, target, alias, id, userId)) { - throw new UserInputError('Attempting to delete locked resource'); - } - await ActionLogger.log(entityManger, target, alias, { id: id }, userId, Actions.DELETE); - return await entityManger.getRepository(target) - .createQueryBuilder(alias) - .delete() - .where('id = :id', { id: id }) - .execute().then(value => value.affected === 1); - }); -} +/** + * Some utility functions for the database + */ +export class DBUtils { + /** + * Delete any instance of an entity that implements the Lockable interface. + * It must implement the interface, so it can be be ensured, that the instance is not locked by another user. + * @param connection + * @param target + * @param alias + * @param id + * @param userId + */ + static async deleteEntity (connection: Connection, target: ObjectType, alias: string, id: number, userId: number): Promise { + return await connection.transaction(async (entityManger: EntityManager) => { + if (await LockUtils.isLocked(entityManger, target, alias, id, userId)) { + throw new UserInputError('Attempting to delete locked resource'); + } + await ActionLogger.log(entityManger, target, alias, { id: id }, userId, Actions.DELETE); + return await entityManger.getRepository(target) + .createQueryBuilder(alias) + .delete() + .where('id = :id', { id: id }) + .execute().then(value => value.affected === 1); + }); + } -export async function getAllEntity (connection: Connection, target: ObjectType, alias: string, offset?: number, limit?: number) { - if (offset === null || limit === null) { - return await connection.getRepository(target) - .createQueryBuilder(alias) - .select() - .getMany(); - } else { - return await connection.getRepository(target) - .createQueryBuilder(alias) - .select() - .skip(offset) - .take(limit) - .getMany(); + /** + * Return all instances of the given entity called target. + * When offset or limit is not specified, both values are ignored. + * @param connection + * @param target + * @param alias + * @param offset + * @param limit + */ + static async getAllEntity (connection: Connection, target: ObjectType, alias: string, offset?: number, limit?: number) { + if (offset === null || limit === null) { + return await connection.getRepository(target) + .createQueryBuilder(alias) + .select() + .getMany(); + } else { + return await connection.getRepository(target) + .createQueryBuilder(alias) + .select() + .skip(offset) + .take(limit) + .getMany(); + } } } +/** + * Some static functions for the locking feature. + */ export class LockUtils { - static async findById (connection: Connection, target: ObjectType, alias: string, id: number): Promise { + /** + * A helper function to find an instance of any entity that implements Lockable. + * It will throw an error, if nothing is found. + * Using this function only makes sense to use in the context of locking because there is no point in locking + * an instance that does not exist. + * @param connection + * @param target + * @param alias + * @param id + * @private + */ + private static async findById (connection: Connection, target: ObjectType, alias: string, id: number): Promise { return await connection.getRepository(target) .createQueryBuilder(alias) .select() @@ -91,6 +132,17 @@ export class LockUtils { }); } + /** + * Lock an instance of an entity target that implements the Lockable interface and return that instance. + * If lock could not be set, it will still return the entity. + * If lock was set or not can be obtained by the field isLockedByMe in the graphql interface, + * or the the fields lockedBy and lockedUntil in the database. + * @param connection + * @param target + * @param alias + * @param id + * @param userId + */ static async lockEntity (connection: Connection, target: ObjectType, alias: string, id: number, userId: number): Promise { const lock = await connection.getRepository(target) .createQueryBuilder(alias) @@ -119,6 +171,17 @@ export class LockUtils { return await this.findById(connection, target, alias, id); } + /** + * Unlock an instance of an entity target that implements the Lockable interface and return that instance. + * If lock could not be unset, it will still return the entity. + * If lock was set or not can be obtained by the field isLockedByMe in the graphql interface, + * or the the fields lockedBy and lockedUntil in the database. + * @param connection + * @param target + * @param alias + * @param id + * @param userId + */ static async unlockEntity (connection: Connection, target: ObjectType, alias: string, id: number, userId: number): Promise { await connection.getRepository(target) .createQueryBuilder(alias) @@ -161,7 +224,18 @@ export class LockUtils { } } +/** + * Some utility function for the logging features. + */ export class ActionLogger { + /** + * Create array of strings, that can be used to select them form the database. + * If you want to avoid logging all old values, for an update, but only the ones that are updated, + * use this function. If updates are null, ['*'] will be returned. Use this for delete actions. + * @param updates + * @param alias + * @private + */ private static buildSelect (updates: any, alias: string) : string[] { // this hacky shit makes it possible to select subfields like the address or insurance data. Only one layer at the moment if (updates === null) { @@ -181,6 +255,16 @@ export class ActionLogger { return ret; } + /** + * Insert an entry in the log. The log ist just another entity in the database. + * You can only use this in a transaction. So you have to pass an entity manager. + * @param em + * @param target + * @param alias + * @param updates + * @param userId + * @param action + */ static async log (em: EntityManager, target: ObjectType, alias: string, updates: any, userId: number, action: Actions = Actions.UPDATE) { const oldValues = await em.getRepository(target).createQueryBuilder(alias) .select(this.buildSelect(updates, alias)) diff --git a/src/datasources/db/workshopAPI.ts b/src/datasources/db/workshopAPI.ts index 7c28cd2..c6ba9d8 100644 --- a/src/datasources/db/workshopAPI.ts +++ b/src/datasources/db/workshopAPI.ts @@ -21,7 +21,7 @@ import { DataSource } from 'apollo-datasource'; import { Connection, EntityManager, getConnection } from 'typeorm'; import { WorkshopType } from '../../model/WorkshopType'; import { Workshop } from '../../model/Workshop'; -import { ActionLogger, deleteEntity, getAllEntity, LockUtils } from './utils'; +import { ActionLogger, DBUtils, LockUtils } from './utils'; import { UserInputError } from 'apollo-server-express'; import { GraphQLError } from 'graphql'; import { Participant } from '../../model/Participant'; @@ -73,7 +73,7 @@ export class WorkshopAPI extends DataSource { } async deleteWorkshop (id: number, userId: number) { - return await deleteEntity(this.connection, Workshop, 'w', id, userId); + return await DBUtils.deleteEntity(this.connection, Workshop, 'w', id, userId); } async createWorkshopType (workshopType: any) { @@ -115,7 +115,7 @@ export class WorkshopAPI extends DataSource { } async deleteWorkshopType (id: number, userId: number) { - return await deleteEntity(this.connection, WorkshopType, 'wt', id, userId); + return await DBUtils.deleteEntity(this.connection, WorkshopType, 'wt', id, userId); } async workshopTypeById (id: number) { @@ -127,7 +127,7 @@ export class WorkshopAPI extends DataSource { } async workshopTypes (offset?: number, limit?: number) { - return getAllEntity(this.connection, WorkshopType, 'wt', offset, limit); + return DBUtils.getAllEntity(this.connection, WorkshopType, 'wt', offset, limit); } async workshopById (id: number) { @@ -144,7 +144,7 @@ export class WorkshopAPI extends DataSource { * @param limit */ async workshops (offset?: number, limit?: number) { - return await getAllEntity(this.connection, Workshop, 'w', offset, limit); + return await DBUtils.getAllEntity(this.connection, Workshop, 'w', offset, limit); } async trainer1ByWorkshopId (id: number) { diff --git a/src/model/CargoBike.ts b/src/model/CargoBike.ts index 2ea30b0..81fb218 100644 --- a/src/model/CargoBike.ts +++ b/src/model/CargoBike.ts @@ -183,7 +183,9 @@ export class CargoBike implements Lockable { }) group: Group; - @Column() + @Column({ + unique: true + }) name: string; @OneToMany(type => Equipment, equipment => equipment.cargoBikeId, { diff --git a/src/model/EngagementType.ts b/src/model/EngagementType.ts index 01469f2..cdd816e 100644 --- a/src/model/EngagementType.ts +++ b/src/model/EngagementType.ts @@ -26,7 +26,9 @@ export class EngagementType implements Lockable { @PrimaryGeneratedColumn() id: number; - @Column() + @Column({ + unique: true + }) name: string; @Column({ diff --git a/src/model/EquipmentType.ts b/src/model/EquipmentType.ts index 38647f6..f8fd0b7 100644 --- a/src/model/EquipmentType.ts +++ b/src/model/EquipmentType.ts @@ -25,7 +25,9 @@ export class EquipmentType implements Lockable { @PrimaryGeneratedColumn() id: number; - @Column() + @Column({ + unique: true + }) name: string; @Column({ diff --git a/src/model/Organisation.ts b/src/model/Organisation.ts index 279d47c..80857d5 100644 --- a/src/model/Organisation.ts +++ b/src/model/Organisation.ts @@ -28,7 +28,9 @@ export class Organisation implements Lockable { @PrimaryGeneratedColumn() id: number; - @Column() + @Column({ + unique: true + }) name: string; @OneToMany(type => LendingStation, lendingStation => lendingStation.organisationId) diff --git a/src/model/Person.ts b/src/model/Person.ts index 9d1fafa..e383969 100644 --- a/src/model/Person.ts +++ b/src/model/Person.ts @@ -18,10 +18,11 @@ This file is part of fLotte-API-Server. */ import { Lockable } from './CargoBike'; -import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn, Unique } from 'typeorm'; import { ContactInformation } from './ContactInformation'; @Entity() +@Unique(['firstName', 'name']) export class Person implements Lockable { @PrimaryGeneratedColumn() id: number; diff --git a/src/model/WorkshopType.ts b/src/model/WorkshopType.ts index 3322da2..40d922e 100644 --- a/src/model/WorkshopType.ts +++ b/src/model/WorkshopType.ts @@ -26,7 +26,9 @@ export class WorkshopType implements Lockable { @PrimaryGeneratedColumn() id: number; - @Column() + @Column({ + unique: true + }) name: string; @OneToMany(type => Workshop, workshop => workshop.workshopTypeId)