From 595f66f3dc0e3b595656dc677f31c605680d4768 Mon Sep 17 00:00:00 2001 From: leonnicolas Date: Tue, 6 Oct 2020 19:44:19 +0200 Subject: [PATCH] new generic lock/unlock functions --- src/datasources/db/cargobikeAPI.ts | 74 ++--- src/datasources/db/lendingstationAPI.ts | 45 ++- src/datasources/db/providerAPI.ts | 9 + src/datasources/db/utils.ts | 78 +++-- src/index.ts | 2 + src/model/BikeEvent.ts | 15 +- src/model/Engagement.ts | 15 +- src/model/LendingStation.ts | 14 +- src/model/Organisation.ts | 14 +- src/model/Participant.ts | 14 +- src/model/TimeFrame.ts | 15 +- src/resolvers/cargobikeResolver.ts | 50 ++- src/resolvers/contactinformationResolvers.ts | 10 +- src/resolvers/lendingstationResolvers.ts | 27 +- src/resolvers/participantResolvers.ts | 7 +- src/resolvers/providerResolvers.ts | 7 +- src/resolvers/workshopResolvers.ts | 7 +- src/schema/type-defs.ts | 329 ++++++++++--------- 18 files changed, 467 insertions(+), 265 deletions(-) diff --git a/src/datasources/db/cargobikeAPI.ts b/src/datasources/db/cargobikeAPI.ts index 92a9136..241f595 100644 --- a/src/datasources/db/cargobikeAPI.ts +++ b/src/datasources/db/cargobikeAPI.ts @@ -32,7 +32,7 @@ export class CargoBikeAPI extends DataSource { } /** - * Finds cargo bike by id, retuns null if id was not found + * Finds cargo bike by id, returns null if id was not found * @param param0 id of bike */ async findCargoBikeById (id: number) { @@ -67,40 +67,16 @@ export class CargoBikeAPI extends DataSource { .loadOne(); } - async lockCargoBike (id: number, req: any, dataSources: any) { - if (await LockUtils.lockEntity(this.connection, CargoBike, 'cargobike', id, req, dataSources)) { - return this.findCargoBikeById(id); - } else { - return new GraphQLError('CargoBike is locked by other user'); - } - } - - async unlockCargoBike (id: number, req: any, dataSources: any) { - return this.unlockEntity(CargoBike, 'cargobike', id, req, dataSources); - } - - async lockEntity (target: ObjectType, alias: string, id: number, req: any, dataSources: any) { - return LockUtils.lockEntity(this.connection, target, alias, id, req, dataSources); - } - - async unlockEntity (target: ObjectType, alias: string, id: number, req: any, dataSources: any) { - return LockUtils.unlockEntity(this.connection, target, alias, id, req, dataSources); - } - - async isLocked (id: number, req: any, dataSources: any) { - return LockUtils.isLocked(this.connection, CargoBike, 'cargobike', id, req, dataSources); - } - /** * Updates CargoBike and return updated cargoBike * @param param0 cargoBike to be updated */ - async updateCargoBike (cargoBike: any, req: any, dataSources: any) { + async updateCargoBike (cargoBike: any, userId:number) { // TODO lock cargoBike can return error to save one sql query, this will be a complex sql query if (!await this.checkId(CargoBike, 'cargobike', cargoBike.id)) { return new GraphQLError('ID not found'); } - if (!await this.lockCargoBike(cargoBike.id, req, dataSources)) { + if (!await LockUtils.lockEntity(this.connection, CargoBike, 'cb', cargoBike.id, userId)) { return new GraphQLError('Bike locked by other user'); } const keepLock = cargoBike?.keepLock; @@ -124,7 +100,7 @@ export class CargoBikeAPI extends DataSource { .of(cargoBike.id) .addAndRemove(equipmentTypeIds, await this.equipmentTypeByCargoBikeId(cargoBike.id)); // TODO remove all existing relations }); - !keepLock && await this.unlockCargoBike(cargoBike.id, req, dataSources); + !keepLock && await LockUtils.unlockEntity(this.connection, CargoBike, 'cb', cargoBike.id, userId); return await this.findCargoBikeById(cargoBike.id); } @@ -183,6 +159,16 @@ export class CargoBikeAPI extends DataSource { .loadOne(); } + async bikeEventsByCargoBikeId (id: number, offset: number = 0, limit:number = 100) { + return await this.connection.getRepository(CargoBike) + .createQueryBuilder('cb') + .skip(offset) + .take(limit) + .relation(CargoBike, 'bikeEvents') + .of(id) + .loadMany(); + } + async createBikeEventType (bikeEventType: any) { return (await this.connection.getRepository(BikeEventType) .createQueryBuilder('bet') @@ -229,6 +215,14 @@ export class CargoBikeAPI extends DataSource { .getOne(); } + async lockBikeEvent (id: number, userId: number) { + return await LockUtils.lockEntity(this.connection, BikeEvent, 'be', id, userId); + } + + async unlockBikeEvent (id: number, userId: number) { + return await LockUtils.unlockEntity(this.connection, BikeEvent, 'be', id, userId); + } + async findEquipmentById (id: number) { return await this.connection.getRepository(Equipment) .createQueryBuilder('equipment') @@ -248,6 +242,12 @@ export class CargoBikeAPI extends DataSource { return result === 1; } + /** + * Returns equipment of one cargoBike + * @param offset + * @param limit + * @param id + */ async equipmentByCargoBikeId (offset: number, limit: number, id: number) { return await this.connection.getRepository(Equipment) .createQueryBuilder('equipment') @@ -282,16 +282,12 @@ export class CargoBikeAPI extends DataSource { .getOne())?.cargoBike; } - async lockEquipment (id: number, req: any, dataSources: any) { - if (await this.lockEntity(Equipment, 'equipment', id, req, dataSources)) { - return this.findEquipmentById(id); - } else { - return new GraphQLError('Equipment locked by other user'); - } + async lockEquipment (id: number, userId: number) { + return LockUtils.lockEntity(this.connection, Equipment, 'e', id, userId); } - async unlockEquipment (id: number, req: any, dataSources: any) { - return await this.unlockEntity(Equipment, 'equipment', id, req, dataSources); + async unlockEquipment (id: number, userId: number) { + return await LockUtils.unlockEntity(this.connection, Equipment, 'equipment', id, userId); } /** @@ -299,12 +295,12 @@ export class CargoBikeAPI extends DataSource { * Will return updated Equipment joined with CargoBike only if cargoBike is was set in param0 * @param param0 struct with equipment properites */ - async updateEquipment (equipment: any, req: any, dataSources: any) { + async updateEquipment (equipment: any, userId: number) { // TODO let lock cargoBike can return error to save one sql query, this will be a complex sql query if (!await this.checkId(Equipment, 'alias', equipment.id)) { return new GraphQLError('ID not found in DB'); } - if (!await this.lockEntity(Equipment, 'equipment', equipment.id, req, dataSources)) { + if (!await LockUtils.lockEntity(this.connection, Equipment, 'equipment', equipment.id, userId)) { return new GraphQLError('Equipment locked by other user'); } const keepLock = equipment.keepLock; @@ -324,7 +320,7 @@ export class CargoBikeAPI extends DataSource { .relation(Equipment, 'cargoBike') .of(equipment.id) .set(cargoBikeId); - !keepLock && this.unlockCargoBike(equipment.id, req, dataSources); + !keepLock && LockUtils.unlockEntity(this.connection, Equipment, 'e', equipment.id, userId); return this.findEquipmentById(equipment.id); } return this.findEquipmentById(equipment.id); diff --git a/src/datasources/db/lendingstationAPI.ts b/src/datasources/db/lendingstationAPI.ts index 09f1108..887d241 100644 --- a/src/datasources/db/lendingstationAPI.ts +++ b/src/datasources/db/lendingstationAPI.ts @@ -5,6 +5,7 @@ import { Connection, EntityManager, getConnection, QueryFailedError } from 'type import { CargoBike } from '../../model/CargoBike'; import { LendingStation } from '../../model/LendingStation'; import { TimeFrame } from '../../model/TimeFrame'; +import { LockUtils } from './utils'; export class LendingStationAPI extends DataSource { connection : Connection @@ -81,6 +82,26 @@ export class LendingStationAPI extends DataSource { .getMany().catch(() => { return []; }); } + async timeFrameById (id: number) { + return await this.connection.getRepository(TimeFrame) + .createQueryBuilder('tf') + .select() + .where('id = :id', { id: id }) + .getOne(); + } + + async lockLendingStationById (id: number, uId: number) { + return await LockUtils.lockEntity(this.connection, LendingStation, 'ls', id, uId); + } + + unlockLendingStationById (id: number, uId: number) { + return LockUtils.unlockEntity(this.connection, LendingStation, 'ls', id, uId); + } + + async lockTimeFrame (id: number, userId: number) { + return await LockUtils.lockEntity(this.connection, TimeFrame, 'tf', id, userId); + } + /** * Counts all timeframes with one lendingStation that overlap with today's date * @param id of lendingStation @@ -115,22 +136,14 @@ export class LendingStationAPI extends DataSource { */ async createLendingStation (lendingStation: any) { let inserts: any; - try { - await this.connection.transaction(async entiyManager => { - inserts = await entiyManager.createQueryBuilder(LendingStation, 'lendingstation') - .insert() - .values([lendingStation]) - .returning('*') - .execute(); - await entiyManager.getRepository(LendingStation) - .createQueryBuilder('lendingstation') - .relation(LendingStation, 'contactPersons') - .of(lendingStation.id) - .add(lendingStation?.contactPersonIds.map((e: any) => { return Number(e); })); - }); - } catch (e :any) { - return new GraphQLError('Transaction could not be completed'); - } + await this.connection.transaction(async entiyManager => { + inserts = await entiyManager.createQueryBuilder(LendingStation, 'lendingstation') + .insert() + .values([lendingStation]) + .returning('*') + .execute(); + }); + const newLendingStaion = inserts.generatedMaps[0]; newLendingStaion.id = inserts.identifiers[0].id; return newLendingStaion; diff --git a/src/datasources/db/providerAPI.ts b/src/datasources/db/providerAPI.ts index 3a5232a..0e55ec8 100644 --- a/src/datasources/db/providerAPI.ts +++ b/src/datasources/db/providerAPI.ts @@ -3,6 +3,7 @@ import { Connection, EntityManager, getConnection } from 'typeorm'; import { Provider } from '../../model/Provider'; import { Organisation } from '../../model/Organisation'; import { UserInputError } from 'apollo-server'; +import { CargoBike } from '../../model/CargoBike'; export class ProviderAPI extends DataSource { connection : Connection @@ -36,6 +37,14 @@ export class ProviderAPI extends DataSource { .getOne(); } + async providerByCargoBikeId (id: number) { + return await this.connection.getRepository(CargoBike) + .createQueryBuilder('cb') + .relation(CargoBike, 'provider') + .of(id) + .loadOne(); + } + async organisationByProviderId (id: number) { return await this.connection.getRepository(Provider) .createQueryBuilder() diff --git a/src/datasources/db/utils.ts b/src/datasources/db/utils.ts index af0b0f3..fab64ac 100644 --- a/src/datasources/db/utils.ts +++ b/src/datasources/db/utils.ts @@ -1,5 +1,6 @@ import { Connection, ObjectType } from 'typeorm'; import { CargoBike, Lockable } from '../../model/CargoBike'; +import { GraphQLError } from 'graphql'; export function genDateRange (struct: any) { if (struct.to === undefined) { @@ -14,23 +15,33 @@ export function genDateRange (struct: any) { } } +/** + * 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 + */ +export function isLocked (parent: any, { dataSources, req }: { dataSources: any; req: any }) { + return dataSources.userAPI.getUserId(LockUtils.getToken(req)).then((value: number) => { + return value !== parent.lockedBy && new Date() <= new Date(parent.lockedUntil); + }); +} + export class LockUtils { static getToken (req: any) : string { return req.headers.authorization?.replace('Bearer ', ''); } - /** - * Locks any Entity, that - * @param connection - * @param target - * @param alias - * @param id - * @param req - * @param dataSources - */ - static async lockEntity (connection: Connection, target: ObjectType, alias: string, id: number, req: any, dataSources: any) { - const token = this.getToken(req); - const userId = await dataSources.userAPI.getUserId(token); + static async findById (connection: Connection, target: ObjectType, alias: string, id: number, userId: number): Promise { + return await connection.getRepository(target) + .createQueryBuilder(alias) + .select() + .where(alias + '.id = :id', { id: id }) + .getOne(); + } + + static async lockEntity (connection: Connection, target: ObjectType, alias: string, id: number, userId: number): Promise { const lock = await connection.getRepository(target) .createQueryBuilder(alias) .select([ @@ -44,7 +55,7 @@ export class LockUtils { .getOne(); // eslint-disable-next-line eqeqeq if (!lock?.lockedUntil || lock?.lockedBy == userId) { - // no lock -> set lock + // no lock -> set lock await connection.getRepository(target) .createQueryBuilder(alias) .update() @@ -54,25 +65,14 @@ export class LockUtils { }) .where('id = :id', { id: id }) .execute(); - return true; + return await this.findById(connection, target, alias, id, userId); } else { - // lock was set - return false; + // lock was set + throw new GraphQLError('Entry locked by other user'); } } - /** - * Unlocks any entity that implements Lockable. - * @param connection - * @param target - * @param alias - * @param id - * @param req - * @param dataSources - */ - static async unlockEntity (connection: Connection, target: ObjectType, alias: string, id: number, req: any, dataSources: any) { - const token = this.getToken(req); - const userId = await dataSources.userAPI.getUserId(token); + static async unlockEntity (connection: Connection, target: ObjectType, alias: string, id: number, userId: number): Promise { const lock = await connection.getRepository(target) .createQueryBuilder(alias) .select([ @@ -115,9 +115,7 @@ export class LockUtils { * @param req * @param dataSources */ - static async isLocked (connection: Connection, target: ObjectType, alias: string, id: number, req: any, dataSources: any) { - const token = this.getToken(req); - const userId = await dataSources.userAPI.getUserId(token); + static async isLocked (connection: Connection, target: ObjectType, alias: string, id: number, userId: number) { const lock = await connection.getRepository(CargoBike) .createQueryBuilder(alias) .select([ @@ -135,4 +133,22 @@ export class LockUtils { // eslint-disable-next-line eqeqeq } else return lock?.lockedBy != userId; } + + /** + * Returns true if id is found in database + * @param connection + * @param target + * @param alias + * @param id + */ + static async checkId (connection: Connection, target: ObjectType, alias: string, id: number) { + const result = await connection.getRepository(target) + .createQueryBuilder(alias) + .select([ + alias + '.id' + ]) + .where('id = :id', { id: id }) + .getCount(); + return result === 1; + } } diff --git a/src/index.ts b/src/index.ts index f434e7d..370f838 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,12 +45,14 @@ require('dotenv').config(); async function authenticate (req: any, res: any, next: any) { if (process.env.NODE_ENV === 'develop') { req.permissions = requiredPermissions.map((e) => e.name); + req.userId = await userAPI.getUserId(req.headers.authorization?.replace('Bearer ', '')); next(); } else { const token = req.headers.authorization?.replace('Bearer ', ''); if (token) { if (await userAPI.validateToken(token)) { req.permissions = await userAPI.getUserPermissions(token); + req.userId = await userAPI.getUserId(req.headers.authorization?.replace('Bearer ', '')); next(); } else { res.status(401); diff --git a/src/model/BikeEvent.ts b/src/model/BikeEvent.ts index a0575cc..64a0894 100644 --- a/src/model/BikeEvent.ts +++ b/src/model/BikeEvent.ts @@ -1,12 +1,12 @@ /* eslint no-unused-vars: "off" */ import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn, TreeLevelColumn } from 'typeorm'; -import { CargoBike } from './CargoBike'; +import { CargoBike, Lockable } from './CargoBike'; import { BikeEventType } from './BikeEventType'; import { Participant } from './Participant'; import { type } from 'os'; @Entity() -export class BikeEvent { +export class BikeEvent implements Lockable { @PrimaryGeneratedColumn() id: number; @@ -61,4 +61,15 @@ export class BikeEvent { name: 'bikeEventTypeId' }) bikeEventTypeId: number; + + @Column({ + nullable: true, + type: 'timestamp' + }) + lockedUntil: Date; + + @Column({ + nullable: true + }) + lockedBy: number; } diff --git a/src/model/Engagement.ts b/src/model/Engagement.ts index 0baa813..621e819 100644 --- a/src/model/Engagement.ts +++ b/src/model/Engagement.ts @@ -1,10 +1,10 @@ import { Entity, PrimaryGeneratedColumn, ManyToOne, Column, JoinColumn } from 'typeorm'; import { Participant } from './Participant'; -import { CargoBike } from './CargoBike'; +import { CargoBike, Lockable } from './CargoBike'; import { EngagementType } from './EngagementType'; @Entity() -export class Engagement { +export class Engagement implements Lockable { @PrimaryGeneratedColumn() id: number; @@ -62,4 +62,15 @@ export class Engagement { default: false }) roleBringer: boolean; + + @Column({ + nullable: true, + type: 'timestamp' + }) + lockedUntil: Date; + + @Column({ + nullable: true + }) + lockedBy: number; } diff --git a/src/model/LendingStation.ts b/src/model/LendingStation.ts index d5004fa..9897da3 100644 --- a/src/model/LendingStation.ts +++ b/src/model/LendingStation.ts @@ -3,6 +3,7 @@ import { TimeFrame } from './TimeFrame'; import { Organisation } from './Organisation'; import { Address } from './Provider'; import { ContactInformation } from './ContactInformation'; +import { Lockable } from './CargoBike'; export class LoanPeriod { /** @@ -30,7 +31,7 @@ export class LoanPeriod { } @Entity() -export class LendingStation { +export class LendingStation implements Lockable { @PrimaryGeneratedColumn() id: number; @@ -63,4 +64,15 @@ export class LendingStation { name: 'organisationId' }) organisationId: number; + + @Column({ + nullable: true, + type: 'timestamp' + }) + lockedUntil: Date; + + @Column({ + nullable: true + }) + lockedBy: number; } diff --git a/src/model/Organisation.ts b/src/model/Organisation.ts index 2bcffb4..5e24fb7 100644 --- a/src/model/Organisation.ts +++ b/src/model/Organisation.ts @@ -2,9 +2,10 @@ import { PrimaryGeneratedColumn, OneToOne, OneToMany, Column, Entity, JoinColumn import { LendingStation } from './LendingStation'; import { Address, Provider } from './Provider'; import { ContactInformation } from './ContactInformation'; +import { Lockable } from './CargoBike'; @Entity() -export class Organisation { +export class Organisation implements Lockable { @PrimaryGeneratedColumn() id: number; @@ -38,4 +39,15 @@ export class Organisation { @Column(type => Address) address: Address; + + @Column({ + nullable: true, + type: 'timestamp' + }) + lockedUntil: Date; + + @Column({ + nullable: true + }) + lockedBy: number; } diff --git a/src/model/Participant.ts b/src/model/Participant.ts index 0c66c95..2774722 100644 --- a/src/model/Participant.ts +++ b/src/model/Participant.ts @@ -2,9 +2,10 @@ import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn, OneToMany import { ContactInformation } from './ContactInformation'; import { Engagement } from './Engagement'; import { Workshop } from './Workshop'; +import { Lockable } from './CargoBike'; @Entity() -export class Participant { +export class Participant implements Lockable { @PrimaryGeneratedColumn() id: number; @@ -68,4 +69,15 @@ export class Participant { default: false }) memberADFC: boolean; + + @Column({ + nullable: true, + type: 'timestamp' + }) + lockedUntil: Date; + + @Column({ + nullable: true + }) + lockedBy: number; } diff --git a/src/model/TimeFrame.ts b/src/model/TimeFrame.ts index 053517b..cc45f9e 100644 --- a/src/model/TimeFrame.ts +++ b/src/model/TimeFrame.ts @@ -1,12 +1,12 @@ import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; import { LendingStation } from './LendingStation'; -import { CargoBike } from './CargoBike'; +import { CargoBike, Lockable } from './CargoBike'; /** * When was a cargoBike at what lendingStation */ @Entity() -export class TimeFrame { +export class TimeFrame implements Lockable { @PrimaryGeneratedColumn() id: number; @@ -25,4 +25,15 @@ export class TimeFrame { @ManyToOne(type => CargoBike, cargoBike => cargoBike.timeFrames) cargoBike: CargoBike; + + @Column({ + nullable: true, + type: 'timestamp' + }) + lockedUntil: Date; + + @Column({ + nullable: true + }) + lockedBy: number; } diff --git a/src/resolvers/cargobikeResolver.ts b/src/resolvers/cargobikeResolver.ts index 815da7e..81d9ccc 100644 --- a/src/resolvers/cargobikeResolver.ts +++ b/src/resolvers/cargobikeResolver.ts @@ -1,5 +1,6 @@ import { Permission } from '../datasources/userserver/permission'; import { GraphQLError } from 'graphql'; +import { isLocked } from '../datasources/db/utils'; export default { Query: { @@ -59,24 +60,29 @@ export default { lendingStation (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { return dataSources.lendingStationAPI.lendingStationByCargoBikeId(parent.id); }, - isLocked (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { - return dataSources.cargoBikeAPI.isLocked(parent.id, req, dataSources); + bikeEvents (parent: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) { + return dataSources.cargoBikeAPI.bikeEventsByCargoBikeId(parent.id, offset, limit); }, + isLocked: (parent: any, __: any, { dataSources, req }: { dataSources: any; req: any }) => isLocked(parent, { dataSources, req }), lockedBy (): any { return null; }, timeFrames (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { - return dataSources.lendingStationAPI.timeFramesByCargoBikeId(parent.id, req, dataSources); + return dataSources.lendingStationAPI.timeFramesByCargoBikeId(parent.id); }, equipmentType (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { - return dataSources.cargoBikeAPI.equipmentTypeByCargoBikeId(parent.id, req, dataSources); + return dataSources.cargoBikeAPI.equipmentTypeByCargoBikeId(parent.id); + }, + provider (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { + return dataSources.providerAPI.providerByCargoBikeId(parent.id); } }, Equipment: { cargoBike (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { return dataSources.cargoBikeAPI.cargoBikeByEquipmentId(parent.id); - } + }, + isLocked: (parent: any, __: any, { dataSources, req }: { dataSources: any; req: any }) => isLocked(parent, { dataSources, req }) }, BikeEvent: { cargoBike (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { @@ -90,7 +96,11 @@ export default { }, related (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { return dataSources.cargoBikeAPI.relatedByBikeEventId(parent.id); - } + }, + isLocked: (parent: any, __: any, { dataSources, req }: { dataSources: any; req: any }) => isLocked(parent, { dataSources, req }) + }, + BikeEventType: { + isLocked: (parent: any, __: any, { dataSources, req }: { dataSources: any; req: any }) => isLocked(parent, { dataSources, req }) }, Mutation: { createCargoBike: (_: any, { cargoBike }: { cargoBike: any }, { dataSources, req }: { dataSources: any, req: any }) => { @@ -102,21 +112,21 @@ export default { }, lockCargoBikeById: (_: any, { id }: { id: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.WriteBike)) { - return dataSources.cargoBikeAPI.lockCargoBike(id, req, dataSources); + return dataSources.cargoBikeAPI.lockCargoBike(id, req.userId); } else { return new GraphQLError('Insufficient Permissions'); } }, unlockCargoBikeById: (_: any, { id }: { id: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.WriteBike)) { - return dataSources.cargoBikeAPI.unlockCargoBike(id, req, dataSources); + return dataSources.cargoBikeAPI.unlockCargoBike(id, req.userId); } else { return new GraphQLError('Insufficient Permissions'); } }, updateCargoBike: (_: any, { cargoBike }: { cargoBike: any }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.WriteBike)) { - return dataSources.cargoBikeAPI.updateCargoBike(cargoBike, req, dataSources); + return dataSources.cargoBikeAPI.updateCargoBike(cargoBike, req.userId); } else { return new GraphQLError('Insufficient Permissions'); } @@ -128,6 +138,20 @@ export default { return new GraphQLError('Insufficient Permissions'); } }, + lockBikeEventById: (_: any, { id }: { id: number }, { dataSources, req }: { dataSources: any, req: any }) => { + if (req.permissions.includes(Permission.WriteBike)) { + return dataSources.cargoBikeAPI.lockBikeEvent(id, req.userId); + } else { + return new GraphQLError('Insufficient Permissions'); + } + }, + unlockBikeEventById: (_: any, { id }: { id: number }, { dataSources, req }: { dataSources: any, req: any }) => { + if (req.permissions.includes(Permission.WriteBike)) { + return dataSources.cargoBikeAPI.unlockBikeEvent(id, req.userId); + } else { + return new GraphQLError('Insufficient Permissions'); + } + }, createEquipment: (_: any, { equipment }: { equipment: any }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.WriteBike)) { return dataSources.cargoBikeAPI.createEquipment({ equipment }); @@ -137,21 +161,21 @@ export default { }, lockEquipmentById: (_: any, { id }: { id: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.WriteBike)) { - return dataSources.cargoBikeAPI.lockEquipment(id, req, dataSources); + return dataSources.cargoBikeAPI.lockEquipment(id, req.userId); } else { return new GraphQLError('Insufficient Permissions'); } }, - unlockEquipment: (_: any, { id }: { id: number }, { dataSources, req }: { dataSources: any, req: any }) => { + unlockEquipmentById: (_: any, { id }: { id: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.WriteBike)) { - return dataSources.cargoBikeAPI.unlockEquipment(id, req, dataSources); + return dataSources.cargoBikeAPI.unlockEquipment(id, req.userId); } else { return new GraphQLError('Insufficient Permissions'); } }, updateEquipment: (_: any, { equipment }: { equipment: any }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.WriteBike)) { - return dataSources.cargoBikeAPI.updateEquipment(equipment, req, dataSources); + return dataSources.cargoBikeAPI.updateEquipment(equipment, req.userId); } else { return new GraphQLError('Insufficient Permissions'); } diff --git a/src/resolvers/contactinformationResolvers.ts b/src/resolvers/contactinformationResolvers.ts index bfdbcfe..78b7fca 100644 --- a/src/resolvers/contactinformationResolvers.ts +++ b/src/resolvers/contactinformationResolvers.ts @@ -1,6 +1,7 @@ import { GraphQLError } from 'graphql'; import { Permission } from '../datasources/userserver/permission'; import { Person } from '../model/Person'; +import { isLocked } from '../datasources/db/utils'; export default { Query: { @@ -26,7 +27,8 @@ export default { } else { return new GraphQLError('Insufficient Permissions'); } - } + }, + isLocked: (parent: any, __: any, { dataSources, req }: { dataSources: any; req: any }) => isLocked(parent, { dataSources, req }) }, Person: { contactInformation: (parent: Person, __: any, { dataSources, req }: { dataSources: any, req: any }) => { @@ -35,7 +37,8 @@ export default { } else { return new GraphQLError('Insufficient Permissions'); } - } + }, + isLocked: (parent: any, __: any, { dataSources, req }: { dataSources: any; req: any }) => isLocked(parent, { dataSources, req }) }, ContactInformation: { person: (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) => { @@ -44,7 +47,8 @@ export default { } else { return new GraphQLError('Insufficient Permissions'); } - } + }, + isLocked: (parent: any, __: any, { dataSources, req }: { dataSources: any; req: any }) => isLocked(parent, { dataSources, req }) }, Mutation: { createContactPerson: (_: any, { contactPerson }: { contactPerson: any }, { dataSources, req }: { dataSources: any, req: any }) => { diff --git a/src/resolvers/lendingstationResolvers.ts b/src/resolvers/lendingstationResolvers.ts index 813a242..34dd237 100644 --- a/src/resolvers/lendingstationResolvers.ts +++ b/src/resolvers/lendingstationResolvers.ts @@ -1,6 +1,7 @@ import { Permission } from '../datasources/userserver/permission'; import { GraphQLError } from 'graphql'; import { LendingStation } from '../model/LendingStation'; +import { isLocked } from '../datasources/db/utils'; export default { Query: { @@ -35,6 +36,12 @@ export default { }, cargoBikes (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { return dataSources.lendingStationAPI.cargoBikesByLendingStationId(parent.id); + }, + isLocked: (parent: any, __: any, { dataSources, req }: { dataSources: any; req: any }) => isLocked(parent, { dataSources, req }) + }, + LoanPeriod: { + loanTimes (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { + return parent.loanTimes.split(','); } }, TimeFrame: { @@ -50,7 +57,8 @@ export default { }, lendingStation (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { return dataSources.lendingStationAPI.lendingStationByTimeFrameId(parent.id); - } + }, + isLocked: (parent: any, __: any, { dataSources, req }: { dataSources: any; req: any }) => isLocked(parent, { dataSources, req }) }, Mutation: { createLendingStation: (_: any, { lendingStation }:{ lendingStation: LendingStation }, { dataSources, req }:{dataSources: any, req: any }) => { @@ -60,6 +68,16 @@ export default { return new GraphQLError('Insufficient Permissions'); } }, + lockLendingStationById: (_: any, { id }:{ id: number }, { dataSources, req }:{dataSources: any, req: any }) => { + if (req.permissions.includes(Permission.WriteBike)) { + return dataSources.lendingStationAPI.lockLendingStationById(id, req.userId); + } else { + return new GraphQLError('Insufficient Permissions'); + } + }, + unlockLendingStationById: (_: any, { id }:{ id: number }, { dataSources, req }:{dataSources: any, req: any }) => { + return dataSources.lendingStationAPI.unlockLendingStationById(id, req.userId); + }, updateLendingStation: (_: any, { lendingStation }:{ lendingStation: LendingStation }, { dataSources, req }:{dataSources: any, req: any }) => { if (req.permissions.includes(Permission.WriteBike)) { return dataSources.lendingStationAPI.updateLendingStation({ lendingStation }); @@ -73,6 +91,13 @@ export default { } else { return new GraphQLError('Insufficient Permissions'); } + }, + lockTimeFrame: (_: any, { id }:{ id: number }, { dataSources, req }:{dataSources: any, req: any }) => { + if (req.permissions.includes(Permission.WriteBike)) { + return dataSources.lendingStationAPI.lockTimeFrame(id, req.userId); + } else { + return new GraphQLError('Insufficient Permissions'); + } } } }; diff --git a/src/resolvers/participantResolvers.ts b/src/resolvers/participantResolvers.ts index a7b3c3a..88ba876 100644 --- a/src/resolvers/participantResolvers.ts +++ b/src/resolvers/participantResolvers.ts @@ -1,6 +1,7 @@ import { GraphQLError } from 'graphql'; import { Permission } from '../datasources/userserver/permission'; import { EngagementType } from '../model/EngagementType'; +import { isLocked } from '../datasources/db/utils'; export default { Query: { @@ -25,7 +26,8 @@ export default { }, contactInformation (parent: any, _: any, { dataSources, req }: { dataSources: any, req: any }) { return (dataSources.participantAPI.contactInformationByParticipantId(parent.id)); - } + }, + isLocked: (parent: any, __: any, { dataSources, req }: { dataSources: any; req: any }) => isLocked(parent, { dataSources, req }) }, Engagement: { cargoBike (parent: any, _: any, { dataSources, req }: { dataSources: any, req: any }) { @@ -43,7 +45,8 @@ export default { to (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { const str = (parent.dateRange as string).split(',')[1].replace(')', ''); return (str.length > 0) ? str : null; - } + }, + isLocked: (parent: any, __: any, { dataSources, req }: { dataSources: any; req: any }) => isLocked(parent, { dataSources, req }) }, Mutation: { createParticipant: (_: any, { participant }: { participant: any }, { dataSources, req }: { dataSources: any, req: any }) => { diff --git a/src/resolvers/providerResolvers.ts b/src/resolvers/providerResolvers.ts index 249c4b8..bde3f32 100644 --- a/src/resolvers/providerResolvers.ts +++ b/src/resolvers/providerResolvers.ts @@ -1,5 +1,6 @@ import { GraphQLError } from 'graphql'; import { Permission } from '../datasources/userserver/permission'; +import { isLocked } from '../datasources/db/utils'; export default { Query: { @@ -31,7 +32,8 @@ export default { }, privatePerson: (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) => { return dataSources.providerAPI.privatePersonByProviderId(parent.id); - } + }, + isLocked: (parent: any, __: any, { dataSources, req }: { dataSources: any; req: any }) => isLocked(parent, { dataSources, req }) }, Organisation: { provider: (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) => { @@ -39,7 +41,8 @@ export default { }, contactInformation: (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) => { return dataSources.providerAPI.contactInformationByOrganisationId(parent.id); - } + }, + isLocked: (parent: any, __: any, { dataSources, req }: { dataSources: any; req: any }) => isLocked(parent, { dataSources, req }) }, Mutation: { createProvider: (_: any, { provider }: { provider: number }, { dataSources, req }: { dataSources: any, req: any }) => { diff --git a/src/resolvers/workshopResolvers.ts b/src/resolvers/workshopResolvers.ts index 78c9ac1..986caa9 100644 --- a/src/resolvers/workshopResolvers.ts +++ b/src/resolvers/workshopResolvers.ts @@ -1,5 +1,6 @@ import { Permission } from '../datasources/userserver/permission'; import { GraphQLError } from 'graphql'; +import { isLocked } from '../datasources/db/utils'; export default { Query: { @@ -24,7 +25,11 @@ export default { }, trainer2: (parent: any, __:any, { dataSources, req }: { dataSources: any, req: any }) => { return dataSources.workshopAPI.trainer2ByWorkshopId(parent.id); - } + }, + isLocked: (parent: any, __: any, { dataSources, req }: { dataSources: any; req: any }) => isLocked(parent, { dataSources, req }) + }, + WorkshopType: { + isLocked: (parent: any, __: any, { dataSources, req }: { dataSources: any; req: any }) => isLocked(parent, { dataSources, req }) }, Mutation: { createWorkshop: (_: any, { workshop }: { workshop: number }, { dataSources, req }: { dataSources: any, req: any }) => { diff --git a/src/schema/type-defs.ts b/src/schema/type-defs.ts index d9c2cd9..b190370 100644 --- a/src/schema/type-defs.ts +++ b/src/schema/type-defs.ts @@ -31,7 +31,7 @@ type CargoBike { Does not refer to an extra table in the database. """ dimensionsAndLoad: DimensionsAndLoad! - bikeEvents: [BikeEvent] + bikeEvents(offset: Int, limit: Int): [BikeEvent] equipment(offset: Int!, limit: Int!): [Equipment] "Refers to equipment that is not unique. See kommentierte info tabelle -> Fragen -> Frage 2" equipmentType: [EquipmentType] @@ -184,6 +184,116 @@ input InsuranceDataUpdateInput { notes: String } +"How are the dimensions and how much weight can handle a bike. This data is merged in the CargoBike table and the BikeModel table." +type DimensionsAndLoad { + hasCoverBox: Boolean! + lockable: Boolean! + boxLength: Float! + boxWidth: Float! + boxHeight: Float! + maxWeightBox: Float! + maxWeightLuggageRack: Float! + maxWeightTotal: Float! + bikeLength: Float! + bikeWidth: Float + bikeHeight: Float + bikeWeight: Float +} + +input DimensionsAndLoadCreateInput { + hasCoverBox: Boolean! + lockable: Boolean! + boxLength: Float! + boxWidth: Float! + boxHeight: Float! + maxWeightBox: Float! + maxWeightLuggageRack: Float! + maxWeightTotal: Float! + bikeLength: Float! + bikeWidth: Float + bikeHeight: Float + bikeWeight: Float +} + +input DimensionsAndLoadUpdateInput { + hasCoverBox: Boolean + lockable: Boolean + boxLength: Float + boxWidth: Float + boxHeight: Float + maxWeightBox: Float + maxWeightLuggageRack: Float + maxWeightTotal: Float + bikeLength: Float + bikeWidth: Float + bikeHeight: Float + bikeWeight: Float +} + +""" +Some Technical Info about the bike. +This should be 1-1 Relation with the CargoBike. +So no id needed for mutation. One Mutation for the CargoBike will be enough. +""" +type TechnicalEquipment { + bicycleShift: String! + isEBike: Boolean! + hasLightSystem: Boolean! + specialFeatures: String +} + +input TechnicalEquipmentCreateInput { + bicycleShift: String! + isEBike: Boolean! + hasLightSystem: Boolean! + specialFeatures: String +} + +input TechnicalEquipmentUpdateInput { + bicycleShift: String + isEBike: Boolean + hasLightSystem: Boolean + specialFeatures: String +} + +""" +The Security Info about the bike. +his should be 1-1 Relation with the CargoBike. +So no id needed for mutation. One Mutation for the CargoBike will be enough. +""" +type Security { + frameNumber: String! + keyNumberFrameLock: String + keyNumberAXAChain: String + policeCoding: String + adfcCoding: String +} + +input SecurityCreateInput { + frameNumber: String! + keyNumberFrameLock: String + keyNumberAXAChain: String + policeCoding: String + adfcCoding: String +} + +input SecurityUpdateInput { + frameNumber: String + keyNumberFrameLock: String + keyNumberAXAChain: String + policeCoding: String + adfcCoding: String +} + +enum StickerBikeNameState { + OK + IMPROVE + PRODUCED + NONEED + MISSING + UNKNOWN +} + enum Group{ KL LI @@ -195,18 +305,6 @@ enum Group{ TK } -""" -The BikeModel can be used for instantiate new bikes with a given model. -It should only be used to fill in default values. -Even bikes of the same model can have different properties. -""" -type BikeModel { - id: ID! - name: String! - dimensionsAndLoad: DimensionsAndLoad! - technicalEquipment: TechnicalEquipment! -} - type Participant { id: ID! start: Date! @@ -225,6 +323,10 @@ type Participant { """ distributedActiveBikeParte: Boolean! engagement: [Engagement] + isLocked: Boolean! + "null if not locked by other user" + lockedBy: ID + lockedUntil: Date } input ParticipantCreateInput { @@ -250,6 +352,10 @@ type Workshop { workshopType: WorkshopType! trainer1: Participant! trainer2: Participant + isLocked: Boolean! + "null if not locked by other user" + lockedBy: ID + lockedUntil: Date } input WorkshopCreateInput { @@ -264,6 +370,10 @@ input WorkshopCreateInput { type WorkshopType { id: ID! name: String! + isLocked: Boolean! + "null if not locked by other user" + lockedBy: ID + lockedUntil: Date } input WorkshopTypeCreateInput { @@ -274,6 +384,10 @@ type EngagementType { id: ID! name: String! description: String! + isLocked: Boolean! + "null if not locked by other user" + lockedBy: ID + lockedUntil: Date } input EngagementTypeCreateInput { @@ -296,6 +410,10 @@ type Engagement { roleMentor: Boolean! roleAmbulance: Boolean! roleBringer: Boolean! + isLocked: Boolean! + "null if not locked by other user" + lockedBy: ID + lockedUntil: Date } input EngagementCreateInput { @@ -341,16 +459,6 @@ enum OrganisationArea { ZB } -type ChainSwap { - id: ID! - """ - TODO why is this a string" - """ - mechanic: String - timeOfSwap: Date - keyNumberOldAXAChain: String -} - """ This type represents a piece of equipment that represents a real physical object. The object must be unique. So it is possible to tell it apart from similar objects by a serial number. @@ -358,35 +466,27 @@ The object must be unique. So it is possible to tell it apart from similar objec type Equipment { id: ID! serialNo: String! - """ - TODO unclear what this means. tomy fragen - """ - investable: Boolean title: String! description: String cargoBike: CargoBike + isLocked: Boolean! + "null if not locked by other user" + lockedBy: ID + lockedUntil: Date } input EquipmentCreateInput { serialNo: String! - """ - TODO unclear what this means. tomy fragen - """ title: String! description: String - investable: Boolean cargoBikeId: ID } input EquipmentUpdateInput { id: ID! serialNo: String - """ - TODO unclear what this means. tomy fragen - """ title: String description: String - investable: Boolean cargoBikeId: ID "will keep Bike locked if set to true, default = false" keepLock: Boolean @@ -396,6 +496,10 @@ type EquipmentType { id: ID! name: String! description: String! + isLocked: Boolean! + "null if not locked by other user" + lockedBy: ID + lockedUntil: Date } input EquipmentTypeCreateInput { @@ -423,6 +527,10 @@ type BikeEvent { """ documents: [String]! remark: String + isLocked: Boolean! + "null if not locked by other user" + lockedBy: ID + lockedUntil: Date } input BikeEventCreateInput { @@ -442,6 +550,8 @@ input BikeEventCreateInput { type BikeEventType { id: ID! name: String! + isLocked: Boolean! + lockedUntil: Date } input BikeEventTypeInput { @@ -449,116 +559,6 @@ input BikeEventTypeInput { name: String } -"How are the dimensions and how much weight can handle a bike. This data is merged in the CargoBike table and the BikeModel table." -type DimensionsAndLoad { - hasCoverBox: Boolean! - lockable: Boolean! - boxLength: Float! - boxWidth: Float! - boxHeight: Float! - maxWeightBox: Float! - maxWeightLuggageRack: Float! - maxWeightTotal: Float! - bikeLength: Float! - bikeWidth: Float - bikeHeight: Float - bikeWeight: Float -} - -input DimensionsAndLoadCreateInput { - hasCoverBox: Boolean! - lockable: Boolean! - boxLength: Float! - boxWidth: Float! - boxHeight: Float! - maxWeightBox: Float! - maxWeightLuggageRack: Float! - maxWeightTotal: Float! - bikeLength: Float! - bikeWidth: Float - bikeHeight: Float - bikeWeight: Float -} - -input DimensionsAndLoadUpdateInput { - hasCoverBox: Boolean - lockable: Boolean - boxLength: Float - boxWidth: Float - boxHeight: Float - maxWeightBox: Float - maxWeightLuggageRack: Float - maxWeightTotal: Float - bikeLength: Float - bikeWidth: Float - bikeHeight: Float - bikeWeight: Float -} - -""" -Some Technical Info about the bike. -This should be 1-1 Relation with the CargoBike. -So no id needed for mutation. One Mutation for the CargoBike will be enough. -""" -type TechnicalEquipment { - bicycleShift: String! - isEBike: Boolean! - hasLightSystem: Boolean! - specialFeatures: String -} - -input TechnicalEquipmentCreateInput { - bicycleShift: String! - isEBike: Boolean! - hasLightSystem: Boolean! - specialFeatures: String -} - -input TechnicalEquipmentUpdateInput { - bicycleShift: String - isEBike: Boolean - hasLightSystem: Boolean - specialFeatures: String -} - -""" -The Security Info about the bike. -his should be 1-1 Relation with the CargoBike. -So no id needed for mutation. One Mutation for the CargoBike will be enough. -""" -type Security { - frameNumber: String! - keyNumberFrameLock: String - keyNumberAXAChain: String - policeCoding: String - adfcCoding: String -} - -input SecurityCreateInput { - frameNumber: String! - keyNumberFrameLock: String - keyNumberAXAChain: String - policeCoding: String - adfcCoding: String -} - -input SecurityUpdateInput { - frameNumber: String - keyNumberFrameLock: String - keyNumberAXAChain: String - policeCoding: String - adfcCoding: String -} - -enum StickerBikeNameState { - OK - IMPROVE - PRODUCED - NONEED - MISSING - UNKNOWN -} - "(dt. Anbieter) bezieht sich auf die Beziehung einer Person oder Organisation zum Lastenrad" type Provider { id: ID! @@ -566,6 +566,10 @@ type Provider { privatePerson: ContactInformation organisation: Organisation cargoBikes: [CargoBike] + isLocked: Boolean! + "null if not locked by other user" + lockedBy: ID + lockedUntil: Date } "(dt. Anbieter)" @@ -585,6 +589,10 @@ type Person { name: String! firstName: String! contactInformation: [ContactInformation] + isLocked: Boolean! + "null if not locked by other user" + lockedBy: ID + lockedUntil: Date } input PersonCreateInput { @@ -600,6 +608,10 @@ type ContactInformation { email: String email2: String note: String + isLocked: Boolean! + "null if not locked by other user" + lockedBy: ID + lockedUntil: Date } input ContactInformationCreateInput { @@ -626,6 +638,10 @@ type ContactPerson { id: ID! intern: Boolean! contactInformation: ContactInformation! + isLocked: Boolean! + "null if not locked by other user" + lockedBy: ID + lockedUntil: Date } input ContactPersonCreateInput { @@ -652,6 +668,10 @@ type Organisation { provider: Provider contactInformation: ContactInformation otherData: String + isLocked: Boolean! + "null if not locked by other user" + lockedBy: ID + lockedUntil: Date } input OrganisationCreateInput { @@ -680,6 +700,10 @@ type LendingStation { cargoBikes: [CargoBike] "Total amount of cargoBikes currently assigned to the lending station" numCargoBikes: Int! + isLocked: Boolean! + "null if not locked by other user" + lockedBy: ID + lockedUntil: Date } """ @@ -715,7 +739,7 @@ type LoanPeriod { Loan times from and until for each day of the week. Starting with Monday from, Monday to, Tuesday from, ..., Sunday to """ - times: [String] + loanTimes: [String] } """ @@ -729,7 +753,7 @@ input LoanPeriodInput { Loan times from and until for each day of the week. Starting with Monday from, Monday to, Tuesday from, ..., Sunday to """ - times: [String] + loanTimes: [String] } "(dt. Zeitscheibe) When was a bike where" @@ -742,6 +766,10 @@ type TimeFrame { note: String lendingStation: LendingStation! cargoBike: CargoBike! + isLocked: Boolean! + "null if not locked by other user" + lockedBy: ID + lockedUntil: Date } input TimeFrameCreateInput { @@ -828,7 +856,7 @@ type Mutation { "lock equipment returns true if bike is not locked or if it doesnt exist" lockEquipmentById(id: ID!): Equipment! "unlock Equipment, returns true if Bike does not exist" - unlockEquipment(id: ID!): Boolean! + unlockEquipmentById(id: ID!): Boolean! "update Equipment, returns updated equipment. CargoBike will be null, if cargoBikeId is not set. Pass null for cargoBikeIs to delete the relation" updateEquipment(equipment: EquipmentUpdateInput!): Equipment! createEquipmentType(equipmentType: EquipmentTypeCreateInput!): EquipmentType! @@ -837,15 +865,20 @@ type Mutation { creates new lendingStation and returns lendingStation with new ID """ createLendingStation(lendingStation: LendingStationCreateInput): LendingStation! + lockLendingStationById(id: ID!): LendingStation + unlockLendingStationById(id: ID!): Boolean! "updates lendingStation of given ID with supplied fields and returns updated lendingStation" updateLendingStation(lendingStation: LendingStationUpdateInput!): LendingStation! createTimeFrame(timeFrame: TimeFrameCreateInput!): TimeFrame! + lockTimeFrame(id: ID!): TimeFrame """ BIKEEVENT """ createBikeEventType(name: String!): BikeEventType! "creates new BikeEvent" createBikeEvent(bikeEvent: BikeEventCreateInput!): BikeEvent! + lockBikeEventById(id: ID!): BikeEvent + unlockBikeEventById(id: ID!): Boolean! "create participant" createParticipant(participant: ParticipantCreateInput!): Participant! createWorkshopType(workshopType: WorkshopTypeCreateInput!): WorkshopType!