diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..6c3670c --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,37 @@ + +name: Build Docker Image + +on: + push: + branches: [ main, dev ] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + + - name: Copy Repo Files + uses: actions/checkout@v2 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to Portus + uses: docker/login-action@v1 + with: + registry: https://flotte-docker-registry.spdns.org/ + username: ${{ secrets.PORTUS_USERNAME }} + password: ${{ secrets.PORTUS_PASSWORD }} + + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + push: true + tags: flotte-docker-registry.spdns.org/apollo-server:latest diff --git a/.gitignore b/.gitignore index df43218..e59baa1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ node_modules dist .env .idea -.nyc_output +.nyc_output \ No newline at end of file diff --git a/README.md b/README.md index 6a46ead..682dd5f 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,16 @@ Apollo server written in typescript that handles business logic. [![Build Status](https://travis-ci.com/fLotte-meets-HWR-DB/apollo-server.svg?token=YfRmpHAXqyUafCgSEexw&branch=main)](https://travis-ci.com/fLotte-meets-HWR-DB/apollo-server) ## Assumptions -Userserver and postgres are running e.g. with Julius' Docker Compose. +The [flotte-user-management server](https://github.com/fLotte-meets-HWR-DB/flotte-user-management) and postgres are running. Set the [environment variables](#Environment-Variables) accordingly. ## Usage ### Docker +You can build and run a docker image with ```bash docker build -t . -docker run --rm -p 4000:4000 +docker run --rm -p 4000:4000 -e ... ``` -### Compile and run +Don't forget to pass all the [environment variables](#Environment-Variables) with the -e option. +### Compile and Run Install gulp if not installed ```bash npm -g gulp @@ -21,25 +23,28 @@ npm install gulp npm start ``` +You can set the [environment variables](#Environment-Variables) in a _.env_ file. ### For Development Install node\_modules and gulp ```bash npm -g gulp npm install ``` -And start gulp in watch mode +Start gulp in watch mode to recompile the type script ```bash -gulp watch +gulp watchTs ``` -This will watch *.ts files in _./src_ and recompile to _./dist_ and finally restart the server. +This will watch *.ts files in _./src_ and recompile to _./dist_. You will have to restart the server yourself. ## Environment Variables The following environment variables can be used to configure the server: ```bash RPC_HOST=host:port -NODE_ENV=development/porduction +NODE_ENV=develop/production POSTGRES_CONNECTION_URL=postgres://username:password@host:port/database_name ``` -- __RPC_HOST__ is used for the connection with the userserver. +- __RPC_HOST__ is used for the connection with the [flotte-user-management server](https://github.com/fLotte-meets-HWR-DB/flotte-user-management). - __NODE_ENV__ will not check authentication if set to development - __POSTGRES_CONNECTION_URL__ for connection with the postgres database + +If the API server cannot connect to the [flotte-user-management server](https://github.com/fLotte-meets-HWR-DB/flotte-user-management) or the postgres data base. It will try to reconnect in an endless loop. diff --git a/src/datasources/db/cargobikeAPI.ts b/src/datasources/db/cargobikeAPI.ts index 75d2c84..ff47082 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, LockUtils } from './utils'; +import { ActionLogger, DBUtils, genBoxDimensions, LockUtils } from './utils'; import { EquipmentType } from '../../model/EquipmentType'; import { BikeEventType } from '../../model/BikeEventType'; import { UserInputError } from 'apollo-server-express'; @@ -42,14 +42,8 @@ export class CargoBikeAPI extends DataSource { this.connection = getConnection(); } - async getCargoBikes (offset: number, limit: number) { - return await this.connection.createQueryBuilder() - .select('cargoBike') - .from(CargoBike, 'cargoBike') - .orderBy('name', 'ASC') - .offset(offset) - .limit(limit) - .getMany(); + async getCargoBikes (offset?: number, limit?: number) { + return await DBUtils.getAllEntity(this.connection, CargoBike, 'cb', offset, limit); } /** @@ -104,12 +98,13 @@ export class CargoBikeAPI extends DataSource { async updateCargoBike (cargoBike: any, userId:number) { const keepLock = cargoBike?.keepLock; delete cargoBike.keepLock; - delete cargoBike.lendingStationId; - let equipmentTypeIds: any = null; - if (cargoBike.equipmentTypeIds) { - equipmentTypeIds = cargoBike.equipmentTypeIds; - delete cargoBike.equipmentTypeIds; - } + const equipmentTypeIds = cargoBike?.equipmentTypeIds; + delete cargoBike?.equipmentTypeIds; + const equipmentIds = cargoBike?.equipmentIds; + delete cargoBike?.equipmentIds; + // generate ranges for box dimensions + genBoxDimensions(cargoBike); + await this.connection.transaction(async (entityManager: EntityManager) => { if (await LockUtils.isLocked(entityManager, CargoBike, 'cb', cargoBike.id, userId)) { throw new GraphQLError('CargoBike locked by other user'); @@ -125,7 +120,12 @@ export class CargoBikeAPI extends DataSource { .createQueryBuilder('cb') .relation(CargoBike, 'equipmentTypeIds') .of(cargoBike.id) - .addAndRemove(equipmentTypeIds, await this.equipmentTypeByCargoBikeId(cargoBike.id)); // TODO remove all existing relations + .addAndRemove(equipmentTypeIds, await this.equipmentTypeByCargoBikeId(cargoBike.id)); + equipmentIds && await entityManager.getRepository(CargoBike) + .createQueryBuilder('cb') + .relation(CargoBike, 'equipmentIds') + .of(cargoBike.id) + .addAndRemove(equipmentIds, await this.equipmentByCargoBikeId(cargoBike.id)); }); !keepLock && await LockUtils.unlockEntity(this.connection, CargoBike, 'cb', cargoBike.id, userId); return await this.findCargoBikeById(cargoBike.id); @@ -150,8 +150,9 @@ export class CargoBikeAPI extends DataSource { * created CargoBike and returns created bike with new ID * @param param0 cargoBike to be created */ - async createCargoBike ({ cargoBike }: { cargoBike: any }) { + async createCargoBike (cargoBike: any) { let inserts: any = {}; + genBoxDimensions(cargoBike); await this.connection.transaction(async (entityManager:any) => { inserts = await entityManager.getRepository(CargoBike) .createQueryBuilder('cb') @@ -164,6 +165,11 @@ export class CargoBikeAPI extends DataSource { .relation(CargoBike, 'equipmentTypeIds') .of(inserts.identifiers[0].id) .add(cargoBike.equipmentTypeIds); + cargoBike?.equipmentIds && await entityManager.getRepository(CargoBike) + .createQueryBuilder('cb') + .relation(CargoBike, 'equipmentIds') + .of(inserts.identifiers[0].id) + .add(cargoBike.equipmentIds); }); inserts.generatedMaps[0].id = inserts?.identifiers[0].id; return inserts?.generatedMaps[0]; @@ -198,11 +204,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) { @@ -221,14 +227,22 @@ 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 bikeEventsByCargoBikeId (id: number, offset?: number, limit?: number) { + if (offset === null || limit === null) { + return await this.connection.getRepository(CargoBike) + .createQueryBuilder('cb') + .relation(CargoBike, 'bikeEvents') + .of(id) + .loadMany(); + } else { + return await this.connection.getRepository(CargoBike) + .createQueryBuilder('cb') + .skip(offset) + .take(limit) + .relation(CargoBike, 'bikeEvents') + .of(id) + .loadMany(); + } } async createBikeEventType (bikeEventType: any) { @@ -267,22 +281,12 @@ export class CargoBikeAPI extends DataSource { return await this.bikeEventTypeById(bikeEventType.id); } - async bikeEventTypes (offset: number, limit: number) { - return await this.connection.getRepository(BikeEventType) - .createQueryBuilder('bet') - .select() - .skip(offset) - .take(limit) - .getMany(); + async bikeEventTypes (offset?: number, limit?: number) { + return await DBUtils.getAllEntity(this.connection, BikeEventType, 'bet', offset, limit); } - async bikeEvents (offset: number, limit: number) { - return await this.connection.getRepository(BikeEvent) - .createQueryBuilder('be') - .select() - .skip(offset) - .take(limit) - .getMany(); + async bikeEvents (offset?: number, limit?: number) { + return await DBUtils.getAllEntity(this.connection, BikeEvent, 'be', offset, limit); } async bikeEventTypeById (id: number) { @@ -343,12 +347,22 @@ export class CargoBikeAPI extends DataSource { * @param limit * @param id */ - async equipmentByCargoBikeId (offset: number, limit: number, id: number) { - return await this.connection.getRepository(Equipment) - .createQueryBuilder('equipment') - .select() - .where('equipment."cargoBikeId" = :id', { id: id }) - .getMany(); + async equipmentByCargoBikeId (id: number, offset?: number, limit?: number) { + if (offset == null || limit === null) { + return await this.connection.getRepository(Equipment) + .createQueryBuilder('equipment') + .select() + .where('equipment."cargoBikeId" = :id', { id: id }) + .getMany(); + } else { + return await this.connection.getRepository(Equipment) + .createQueryBuilder('equipment') + .select() + .where('equipment."cargoBikeId" = :id', { id: id }) + .skip(offset) + .take(limit) + .getMany(); + } } async createEquipment ({ equipment }: { equipment: any }) { @@ -406,17 +420,11 @@ 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) { - return await this.connection.getRepository(Equipment) - .createQueryBuilder('equipment') - .leftJoinAndSelect('equipment.cargoBike', 'cargoBike') - .orderBy('title', 'ASC') - .offset(offset) - .limit(limit) - .getMany(); + async getEquipment (offset?: number, limit?: number) { + return await DBUtils.getAllEntity(this.connection, Equipment, 'e', offset, limit); } async createEquipmentType (equipmentType: any) { @@ -460,7 +468,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) { @@ -471,13 +479,8 @@ export class CargoBikeAPI extends DataSource { .getOne(); } - async equipmentTypes (offset: number, limit: number) { - return await this.connection.getRepository(EquipmentType) - .createQueryBuilder('et') - .select() - .skip(offset) - .take(limit) - .getMany(); + async equipmentTypes (offset?: number, limit?: number) { + return await DBUtils.getAllEntity(this.connection, EquipmentType, 'et', offset, limit); } async equipmentTypeByCargoBikeId (id: number) { diff --git a/src/datasources/db/contactinformationAPI.ts b/src/datasources/db/contactinformationAPI.ts index 767b740..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, LockUtils } from './utils'; +import { ActionLogger, DBUtils, LockUtils } from './utils'; import { GraphQLError } from 'graphql'; import { LendingStation } from '../../model/LendingStation'; @@ -32,13 +32,8 @@ export class ContactInformationAPI extends DataSource { this.connection = getConnection(); } - async contactInformation (offset: number, limit: number) { - return await this.connection.getRepository(ContactInformation) - .createQueryBuilder('ci') - .select() - .offset(offset) - .limit(limit) - .getMany(); + async contactInformation (offset?: number, limit?: number) { + return await DBUtils.getAllEntity(this.connection, ContactInformation, 'ci', offset, limit); } async contactInformationById (id: number) { @@ -88,16 +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 this.connection.getRepository(Person) - .createQueryBuilder('person') - .select() - .skip(offset) - .take(limit) - .getMany(); + async persons (offset?: number, limit?: number) { + return await DBUtils.getAllEntity(this.connection, Person, 'p', offset, limit); } async personById (id: number) { @@ -171,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 3df03e1..52c08ad 100644 --- a/src/datasources/db/lendingstationAPI.ts +++ b/src/datasources/db/lendingstationAPI.ts @@ -19,12 +19,11 @@ This file is part of fLotte-API-Server. import { DataSource } from 'apollo-datasource'; import { UserInputError } from 'apollo-server-express'; -import { GraphQLError } from 'graphql'; 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, LockUtils } from './utils'; +import { ActionLogger, genDateRange, DBUtils, LockUtils } from './utils'; export class LendingStationAPI extends DataSource { connection : Connection @@ -44,14 +43,8 @@ export class LendingStationAPI extends DataSource { /** * get all lendingStations */ - async lendingStations (offset: number, limit: number) { - return await this.connection.getRepository(LendingStation) - .createQueryBuilder('lendingStation') - .select() - .offset(offset) - .limit(limit) - .orderBy('name', 'ASC') - .getMany() || new GraphQLError('Internal Server Error: could not query data from table lendingStation'); + async lendingStations (offset?: number, limit?: number) { + return await DBUtils.getAllEntity(this.connection, LendingStation, 'ls', offset, limit); } /** @@ -79,13 +72,8 @@ export class LendingStationAPI extends DataSource { .loadOne(); } - async timeFrames (offset: number, limit: number) { - return await this.connection.getRepository(TimeFrame) - .createQueryBuilder('timeframe') - .select() - .offset(offset) - .limit(limit) - .getMany() || []; + async timeFrames (offset?: number, limit?: number) { + return await DBUtils.getAllEntity(this.connection, TimeFrame, 'tf', offset, limit); } async timeFramesByCargoBikeId (id: number) { @@ -190,15 +178,12 @@ 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) { return await this.connection.transaction(async (entityManager: EntityManager) => { - if (timeFrame.to === undefined) { - timeFrame.to = ''; - } - timeFrame.dateRange = '[' + timeFrame.from + ',' + timeFrame.to + ')'; + genDateRange(timeFrame); // checking for overlapping time frames const overlapping = await entityManager.getRepository(TimeFrame) .createQueryBuilder('timeframe') @@ -268,6 +253,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 e481acc..270ec09 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, LockUtils } from './utils'; +import { ActionLogger, DBUtils, genDateRange, LockUtils } from './utils'; import { UserInputError } from 'apollo-server-express'; import { GraphQLError } from 'graphql'; @@ -42,13 +42,8 @@ export class ParticipantAPI extends DataSource { .getOne(); } - async getParticipants (offset: number, limit: number) { - return await this.connection.getRepository(Participant) - .createQueryBuilder('participant') - .select() - .offset(offset) - .limit(limit) - .getMany(); + async getParticipants (offset?: number, limit?: number) { + return await DBUtils.getAllEntity(this.connection, Participant, 'p', offset, limit); } async participantByEngagementId (id: number) { @@ -84,17 +79,24 @@ export class ParticipantAPI extends DataSource { .getMany(); } - async engagementByCargoBikeId (offset: number, limit: number, id: number) { - return await this.connection.getRepository(Engagement) - .createQueryBuilder('engagement') - .select() - .where('engagement."cargoBikeId" = :id', { - id: id - }) - .skip(offset) - .take(limit) - .orderBy('engagement."dateRange"', 'DESC') - .getMany(); + async engagementByCargoBikeId (id: number, offset?: number, limit?: number) { + if (limit === null || offset === null) { + return await this.connection.getRepository(Engagement) + .createQueryBuilder('engagement') + .select() + .where('engagement."cargoBikeId" = :id', { id: id }) + .orderBy('engagement."dateRange"', 'DESC') + .getMany(); + } else { + return await this.connection.getRepository(Engagement) + .createQueryBuilder('engagement') + .select() + .where('engagement."cargoBikeId" = :id', { id: id }) + .skip(offset) + .take(limit) + .orderBy('engagement."dateRange"', 'DESC') + .getMany(); + } } async currentEngagementByCargoBikeId (id: number) { @@ -107,13 +109,8 @@ export class ParticipantAPI extends DataSource { .getMany(); } - async engagements (offset: number, limit: number) { - return await this.connection.getRepository(Engagement) - .createQueryBuilder('e') - .select() - .skip(offset) - .take(limit) - .getMany(); + async engagements (offset?: number, limit?: number) { + return await DBUtils.getAllEntity(this.connection, Engagement, 'e', offset, limit); } async engagementById (id: number) { @@ -132,13 +129,8 @@ export class ParticipantAPI extends DataSource { .getOne(); } - async engagementTypes (offset: number, limit: number) { - return await this.connection.getRepository(EngagementType) - .createQueryBuilder('et') - .select() - .skip(offset) - .take(limit) - .getMany(); + async engagementTypes (offset?: number, limit?: number) { + return await DBUtils.getAllEntity(this.connection, EngagementType, 'et', offset, limit); } async engagementTypeByEngagementId (id: number) { @@ -178,6 +170,7 @@ export class ParticipantAPI extends DataSource { * @param participant to be created */ async createParticipant (participant: any) { + genDateRange(participant); let inserts: any; await this.connection.transaction(async (entityManager: EntityManager) => { inserts = await entityManager.getRepository(Participant) @@ -187,7 +180,7 @@ export class ParticipantAPI extends DataSource { .values([participant]) .returning('*') .execute(); - await entityManager.getRepository(Participant) + participant.workshopIds && await entityManager.getRepository(Participant) .createQueryBuilder('w') .relation(Participant, 'workshopIds') .of(participant.id) @@ -209,8 +202,9 @@ export class ParticipantAPI extends DataSource { delete participant.keepLock; await this.connection.transaction(async (entityManager: EntityManager) => { if (await LockUtils.isLocked(entityManager, Participant, 'p', participant.id, userId)) { - throw new GraphQLError('Participant is locked by another user'); + throw new UserInputError('Attempting to update locked resource'); } + genDateRange(participant); const workshops = participant.workshopIds; delete participant.workshopIds; await ActionLogger.log(entityManager, Participant, 'p', participant, userId); @@ -219,8 +213,19 @@ export class ParticipantAPI extends DataSource { .update() .set({ ...participant }) .where('id = :id', { id: participant.id }) - .execute().then(value => { if (value.affected !== 1) { throw new GraphQLError('ID not found'); } }); - await entityManager.getRepository(Participant) + .execute().then(value => { if (value.affected !== 1) { throw new UserInputError('ID not found'); } }); + // check for engagements before or after dateRange + const engagements = await entityManager.getRepository(Engagement) + .createQueryBuilder('e') + .select() + .where('e."participantId" = :pid', { pid: participant.id }) + .andWhere('not :pdr @> e."dateRange"', { pdr: participant.dateRange }) + .getMany(); + if (engagements.length !== 0) { + throw new UserInputError('Engagements with ids: ' + engagements.map((e) => { return `${e.id} ,`; }) + ' are are outside of dataRange'); + } + // add and remove workshop relations + workshops && await entityManager.getRepository(Participant) .createQueryBuilder('w') .relation(Participant, 'workshopIds') .of(participant.id) @@ -231,7 +236,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) { @@ -249,6 +254,16 @@ export class ParticipantAPI extends DataSource { if (overlapping.length > 0) { throw new UserInputError('Engagements with ids: ' + overlapping.map((e) => { return e.id + ', '; }) + 'are overlapping'); } + // check if participant is active + const participant = await entityManager.getRepository(Participant) + .createQueryBuilder('p') + .select() + .where('p.id = :pid', { pid: engagement.participantId }) + .andWhere('not p."dateRange" @> :edr', { edr: engagement.dateRange }) + .getOne(); + if (participant) { + throw new UserInputError('Participant ist not active in the specified dateRange'); + } inserts = await entityManager.getRepository(Engagement) .createQueryBuilder('engagement') .insert() @@ -288,6 +303,20 @@ export class ParticipantAPI extends DataSource { if (overlapping.length > 0) { throw new UserInputError('Engagements with ids: ' + overlapping.map((e) => { return e.id + ', '; }) + 'are overlapping'); } + // check if participant is active + if (engagement.dateRange && engagement.participantId) { + const participant = await entityManager.getRepository(Participant) + .createQueryBuilder('p') + .select() + .where('p.id = :pid', { pid: engagement.participantId }) + .andWhere('not p."dateRange" @> :edr', { edr: engagement.dateRange }) + .getOne(); + if (participant) { + throw new UserInputError('Participant ist not active in the specified dateRange'); + } + } else if (engagement.dateRange || engagement.dateRange) { + throw new UserInputError('Please specify participantId adn the dateRange'); + } await entityManager.getRepository(Engagement) .createQueryBuilder('engagement') .update() @@ -300,7 +329,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) { @@ -342,6 +371,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 d8e2c9c..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, LockUtils } from './utils'; +import { ActionLogger, DBUtils, LockUtils } from './utils'; import { GraphQLError } from 'graphql'; export class ProviderAPI extends DataSource { @@ -42,13 +42,8 @@ export class ProviderAPI extends DataSource { .getOne(); } - async provider (offset: number, limit: number) { - return await this.connection.getRepository(Provider) - .createQueryBuilder('provider') - .select() - .skip(offset) - .take(limit) - .getMany(); + async provider (offset?: number, limit?: number) { + return await DBUtils.getAllEntity(this.connection, Provider, 'p', offset, limit); } async providerByOrganisationId (id: number) { @@ -75,13 +70,8 @@ export class ProviderAPI extends DataSource { .loadOne(); } - async organisations (offset: number, limit: number) { - return await this.connection.getRepository(Organisation) - .createQueryBuilder('o') - .select() - .skip(offset) - .limit(limit) - .getMany(); + async organisations (offset?: number, limit?: number) { + return await DBUtils.getAllEntity(this.connection, Organisation, 'o', offset, limit); } async organisationById (id: number) { @@ -180,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) { @@ -223,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 8c56441..7262417 100644 --- a/src/datasources/db/utils.ts +++ b/src/datasources/db/utils.ts @@ -23,49 +23,136 @@ import { ActionLog, Actions } from '../../model/ActionLog'; import { UserInputError } from 'apollo-server-express'; export function genDateRange (struct: any) { - if (struct.to === undefined) { - struct.to = ''; - } - struct.dateRange = '[' + struct.from + ',' + struct.to + ')'; - if (struct.from === undefined) { + if (!struct.dateRange || !struct.dateRange.from) { delete struct.dateRange; + return; + } else if (!struct.dateRange?.to) { + struct.dateRange.to = ''; + } else if (struct.dateRange.to === struct.dateRange.from) { + throw new UserInputError('Date Range can not be empty, provide different dates.'); } - // delete these keys, so the struct can be used to update the engagement entity - delete struct.from; - delete struct.to; + struct.dateRange = `[${struct.dateRange.from},${struct.dateRange.to})`; +} + +/** + * This function helps prepare the cargoBike struct, to be used in an update or create. + * It creates the numrange attributes than can be understood by postgres. + * @param range + */ +function genNumRange (range: { min: number, max: number}) :string { + if (!range || (!range.max && !range.min)) { + return null; + } else if (range.min === null || range.min === undefined) { + range.min = range.max; + } else if (range.max === null || range.max === undefined) { + range.max = range.min; + } + if (range.min < 0) { + throw new UserInputError('Minimal value must be greater or equal to 0'); + } + return `[${range.min},${range.max}]`; +} + +/** + * This function prepares the cargoBike struct, to be used in an update or create. + * It creates the numrange attributes than can be understood by postgres. + * @param cargoBike + */ +export function genBoxDimensions (cargoBike: any) { + if (!cargoBike.dimensionsAndLoad) { return; } + cargoBike.dimensionsAndLoad.boxLengthRange = genNumRange(cargoBike.dimensionsAndLoad.boxLengthRange); + cargoBike.dimensionsAndLoad.boxWidthRange = genNumRange(cargoBike.dimensionsAndLoad.boxWidthRange); + cargoBike.dimensionsAndLoad.boxHeightRange = genNumRange(cargoBike.dimensionsAndLoad.boxHeightRange); } /** - * 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'); +/** + * 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); + }); + } + + /** + * 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(); } - 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 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() @@ -75,6 +162,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) @@ -103,6 +201,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) @@ -145,7 +254,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) { @@ -156,15 +276,25 @@ export class ActionLogger { // sometimes updates[value] is an array, e.g. timePeriods that are saved as a simple array in postgres if (updates[value] && typeof updates[value] === 'object' && !Array.isArray(updates[value])) { Object.keys(updates[value]).forEach(subValue => { - ret.push(alias + '."' + value + subValue[0].toUpperCase() + subValue.substr(1).toLowerCase() + '"'); + ret.push(`${alias}."${value}${subValue[0].toUpperCase()}${subValue.substr(1).toLowerCase()}"`); }); } else { - ret.push(alias + '."' + value + '"'); + ret.push(`${alias}."${value}"`); } }); 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 62b0d07..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, 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) { @@ -126,13 +126,8 @@ export class WorkshopAPI extends DataSource { .getOne(); } - async workshopTypes (offset: number, limit: number) { - return await this.connection.getRepository(WorkshopType) - .createQueryBuilder('w') - .select() - .skip(offset) - .take(limit) - .getMany(); + async workshopTypes (offset?: number, limit?: number) { + return DBUtils.getAllEntity(this.connection, WorkshopType, 'wt', offset, limit); } async workshopById (id: number) { @@ -148,13 +143,8 @@ export class WorkshopAPI extends DataSource { * @param offset * @param limit */ - async workshops (offset: number, limit: number) { - return await this.connection.getRepository(Workshop) - .createQueryBuilder('w') - .select() - .skip(offset) - .take(limit) - .getMany(); + async workshops (offset?: number, limit?: number) { + 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..ccf214c 100644 --- a/src/model/CargoBike.ts +++ b/src/model/CargoBike.ts @@ -30,7 +30,6 @@ import { DeleteDateColumn } from 'typeorm'; import { Provider } from './Provider'; -import { Participant } from './Participant'; import { InsuranceData } from './InsuranceData'; import { TimeFrame } from './TimeFrame'; import { Taxes } from './Taxes'; @@ -66,7 +65,9 @@ export interface Lockable { } export class Security { - @Column() + @Column({ + nullable: true + }) frameNumber: string; @Column({ @@ -90,13 +91,19 @@ export class Security { adfcCoding: string; } export class TechnicalEquipment { - @Column() + @Column({ + nullable: true + }) bicycleShift: string; - @Column() + @Column({ + nullable: true + }) isEBike: boolean; - @Column() + @Column({ + nullable: true + }) hasLightSystem: boolean; @Column({ @@ -106,44 +113,55 @@ export class TechnicalEquipment { } export class DimensionsAndLoad { - @Column() + @Column({ + nullable: true + }) hasCoverBox: boolean; - @Column() + @Column({ + nullable: true + }) lockable:boolean; @Column({ - type: 'decimal' + type: 'numrange', + nullable: true }) - boxLength: number; + boxLengthRange: string; @Column({ - type: 'decimal' + type: 'numrange', + nullable: true }) - boxWidth: number; + boxWidthRange: string; @Column({ - type: 'decimal' + type: 'numrange', + nullable: true }) - boxHeight: number; + boxHeightRange: string; @Column({ - type: 'decimal' + type: 'decimal', + nullable: true }) - maxWeightBox: number; + maxWeightBox: string; @Column({ - type: 'decimal' + type: 'decimal', + nullable: true }) maxWeightLuggageRack: number; @Column({ - type: 'decimal' + type: 'decimal', + nullable: true }) maxWeightTotal: number; @Column({ - type: 'decimal' + type: 'decimal', + nullable: true }) bikeLength: number; @@ -174,23 +192,27 @@ export class CargoBike implements Lockable { @PrimaryGeneratedColumn() id: number; - @DeleteDateColumn() - deleteDate: Date; - @Column({ type: 'enum', enum: Group }) group: Group; - @Column() + @Column({ + unique: true + }) name: string; + @Column({ + nullable: true + }) + state: string; + @OneToMany(type => Equipment, equipment => equipment.cargoBikeId, { nullable: true, eager: true }) - equipment: Equipment[]; + equipmentIds: number[]; // Equipment that is not unique and is supposed to be selected out of a list e.g. drop down @ManyToMany(type => EquipmentType, equipmentType => equipmentType.cargoBikeIds) @@ -245,19 +267,32 @@ export class CargoBike implements Lockable { }) description: string; - @Column() + @Column({ + nullable: true + }) modelName: string; - @Column() + @Column({ + nullable: true + }) numberOfWheels: number; - @Column() + @Column({ + type: 'boolean', + nullable: true + }) forCargo: boolean; - @Column() + @Column({ + type: 'boolean', + nullable: true + }) forChildren: boolean; - @Column() + @Column({ + type: 'int', + nullable: true + }) numberOfChildren: number; @Column(type => TechnicalEquipment) @@ -267,6 +302,7 @@ export class CargoBike implements Lockable { dimensionsAndLoad: DimensionsAndLoad; @Column({ + type: 'int', nullable: true }) lockedBy: number; diff --git a/src/model/ContactInformation.ts b/src/model/ContactInformation.ts index 0e80f27..64e17aa 100644 --- a/src/model/ContactInformation.ts +++ b/src/model/ContactInformation.ts @@ -42,9 +42,7 @@ export class ContactInformation implements Lockable { }) participantId: number; - @Column(type => { - return Address; - }) + @Column(type => { return Address; }) address: Address; @Column({ 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/Equipment.ts b/src/model/Equipment.ts index 91a8b10..6c598f9 100644 --- a/src/model/Equipment.ts +++ b/src/model/Equipment.ts @@ -26,7 +26,9 @@ export class Equipment implements Lockable { @PrimaryGeneratedColumn() id: number; - @Column() + @Column({ + unique: true + }) serialNo: string; @Column() @@ -37,7 +39,7 @@ export class Equipment implements Lockable { }) description: string; - @ManyToOne(type => CargoBike, cargoBike => cargoBike.equipment, { + @ManyToOne(type => CargoBike, cargoBike => cargoBike.equipmentIds, { nullable: true }) @JoinColumn({ 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/InsuranceData.ts b/src/model/InsuranceData.ts index ef7c0fd..27fe256 100644 --- a/src/model/InsuranceData.ts +++ b/src/model/InsuranceData.ts @@ -20,22 +20,34 @@ This file is part of fLotte-API-Server. import { Column } from 'typeorm'; export class InsuranceData { - @Column() + @Column({ + nullable: true + }) name: string; - @Column() + @Column({ + nullable: true + }) benefactor: string; - @Column() + @Column({ + nullable: true + }) billing: string; - @Column() + @Column({ + nullable: true + }) noPnP: string; - @Column() + @Column({ + nullable: true + }) maintenanceResponsible: string; - @Column() + @Column({ + nullable: true + }) maintenanceBenefactor: string; @Column({ diff --git a/src/model/LendingStation.ts b/src/model/LendingStation.ts index 34dc79a..9df1441 100644 --- a/src/model/LendingStation.ts +++ b/src/model/LendingStation.ts @@ -34,6 +34,12 @@ export class LoanPeriod { }) from: Date; + @Column({ + nullable: true, + type: 'text' + }) + generalRemark: string; + /** * validity for loanPeriods */ @@ -44,10 +50,39 @@ export class LoanPeriod { to: Date; @Column({ - type: 'simple-array', nullable: true }) - loanTimes: string[]; + mo: string; + + @Column({ + nullable: true + }) + tu: string; + + @Column({ + nullable: true + }) + we: string; + + @Column({ + nullable: true + }) + th: string; + + @Column({ + nullable: true + }) + fr: string; + + @Column({ + nullable: true + }) + sa: string; + + @Column({ + nullable: true + }) + su: string; } @Entity() 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/Participant.ts b/src/model/Participant.ts index dedf9b8..d623143 100644 --- a/src/model/Participant.ts +++ b/src/model/Participant.ts @@ -29,16 +29,9 @@ export class Participant implements Lockable { id: number; @Column({ - type: 'date', - default: () => 'CURRENT_DATE' + type: 'daterange' }) - start: Date; - - @Column({ - type: 'date', - nullable: true - }) - end: Date; + dateRange: Date[]; @OneToOne(type => ContactInformation, contactInformation => contactInformation.participantId, { nullable: false 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/Taxes.ts b/src/model/Taxes.ts index 3c0f033..449cf27 100644 --- a/src/model/Taxes.ts +++ b/src/model/Taxes.ts @@ -25,7 +25,9 @@ export enum OrganisationArea { ZB = 'ZB' } export class Taxes { - @Column() + @Column({ + nullable: true + }) costCenter: string; @Column({ 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) diff --git a/src/resolvers/cargoBikeResolver.ts b/src/resolvers/cargoBikeResolver.ts index d870fd0..d008537 100644 --- a/src/resolvers/cargoBikeResolver.ts +++ b/src/resolvers/cargoBikeResolver.ts @@ -30,14 +30,14 @@ export default { throw new PermissionError(); } }, - cargoBikes: (_: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) => { + cargoBikes: (_: any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.ReadBike)) { return dataSources.cargoBikeAPI.getCargoBikes(offset, limit); } else { throw new PermissionError(); } }, - bikeEvents: (_:any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) => { + bikeEvents: (_:any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.ReadBikeEvent)) { return dataSources.cargoBikeAPI.bikeEvents(offset, limit); } else { @@ -58,14 +58,14 @@ export default { throw new PermissionError(); } }, - bikeEventTypes: (_:any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) => { + bikeEventTypes: (_:any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.ReadBikeEvent)) { return dataSources.cargoBikeAPI.bikeEventTypes(offset, limit); } else { throw new PermissionError(); } }, - equipment: (_:any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) => { + equipment: (_:any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.ReadEquipment)) { return dataSources.cargoBikeAPI.getEquipment(offset, limit); } else { @@ -79,7 +79,7 @@ export default { throw new PermissionError(); } }, - equipmentTypes: (_:any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) => { + equipmentTypes: (_:any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.ReadEquipment)) { return dataSources.cargoBikeAPI.equipmentTypes(offset, limit); } else { @@ -102,23 +102,23 @@ export default { throw new PermissionError(); } }, - engagement (parent: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) { + engagement (parent: any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) { if (req.permissions.includes(Permission.ReadEngagement)) { - return dataSources.participantAPI.engagementByCargoBikeId(offset, limit, parent.id); + return dataSources.participantAPI.engagementByCargoBikeId(parent.id, offset, limit); } else { throw new PermissionError(); } }, - participants (parent: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) { // TODO should be done with engagements + participants (parent: any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) { // TODO should be done with engagements if (req.permissions.includes(Permission.ReadParticipant)) { return dataSources.participantAPI.participantsByCargoBikeId(parent.id); } else { throw new PermissionError(); } }, - equipment (parent: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) { + equipment (parent: any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) { if (req.permissions.includes(Permission.ReadEquipment)) { - return dataSources.cargoBikeAPI.equipmentByCargoBikeId(offset, limit, parent.id); + return dataSources.cargoBikeAPI.equipmentByCargoBikeId(parent.id, offset, limit); } else { throw new PermissionError(); } @@ -130,15 +130,13 @@ export default { throw new PermissionError(); } }, - bikeEvents (parent: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) { + bikeEvents (parent: any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) { if (req.permissions.includes(Permission.ReadBikeEvent)) { return dataSources.cargoBikeAPI.bikeEventsByCargoBikeId(parent.id, offset, limit); } else { throw new PermissionError(); } }, - isLockedByMe: (parent: any, __: any, { req }: { req: any }) => isLockedByMe(parent, { req }), - isLocked: (parent: any, __: any, { req }: { req: any }) => isLocked(parent, { req }), timeFrames (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { if (req.permissions.includes(Permission.ReadTimeFrame)) { return dataSources.lendingStationAPI.timeFramesByCargoBikeId(parent.id); @@ -159,8 +157,17 @@ export default { } else { throw new PermissionError(); } + }, + isLockedByMe: (parent: any, __: any, { req }: { req: any }) => isLockedByMe(parent, { req }), + isLocked: (parent: any, __: any, { req }: { req: any }) => isLocked(parent, { req }) + }, + NumRange: { + min: (parent: string) => { + return parent.replace(/^\[(.*),.*]$/, '$1'); + }, + max: (parent: string) => { + return parent.replace(/^\[.*,(.*)]$/, '$1'); } - }, Equipment: { cargoBike (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { @@ -173,6 +180,10 @@ export default { isLockedByMe: (parent: any, __: any, { req }: { req: any }) => isLockedByMe(parent, { req }), isLocked: (parent: any, __: any, { req }: { req: any }) => isLocked(parent, { req }) }, + EquipmentType: { + isLockedByMe: (parent: any, __: any, { req }: { req: any }) => isLockedByMe(parent, { req }), + isLocked: (parent: any, __: any, { req }: { req: any }) => isLocked(parent, { req }) + }, BikeEvent: { cargoBike (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { if (req.permissions.includes(Permission.ReadBike)) { @@ -212,7 +223,7 @@ export default { Mutation: { createCargoBike: (_: any, { cargoBike }: { cargoBike: any }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.WriteBike)) { - return dataSources.cargoBikeAPI.createCargoBike({ cargoBike }); + return dataSources.cargoBikeAPI.createCargoBike(cargoBike); } else { throw new PermissionError(); } diff --git a/src/resolvers/contactInformationResolvers.ts b/src/resolvers/contactInformationResolvers.ts index 1ba2bd7..aac36ea 100644 --- a/src/resolvers/contactInformationResolvers.ts +++ b/src/resolvers/contactInformationResolvers.ts @@ -24,7 +24,7 @@ import { PermissionError } from '../errors/PermissionError'; export default { Query: { - contactInformation: (_: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) => { + contactInformation: (_: any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.ReadPerson)) { return dataSources.contactInformationAPI.contactInformation(offset, limit); } else { @@ -45,7 +45,7 @@ export default { throw new PermissionError(); } }, - persons: (_: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) => { + persons: (_: any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.ReadPerson)) { return dataSources.contactInformationAPI.persons(offset, limit); } else { diff --git a/src/resolvers/lendingStationResolvers.ts b/src/resolvers/lendingStationResolvers.ts index a0c872d..e7fe3ee 100644 --- a/src/resolvers/lendingStationResolvers.ts +++ b/src/resolvers/lendingStationResolvers.ts @@ -31,7 +31,7 @@ export default { throw new PermissionError(); } }, - lendingStations: (_: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) => { + lendingStations: (_: any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.ReadLendingStation)) { return dataSources.lendingStationAPI.lendingStations(offset, limit); } else { @@ -45,7 +45,7 @@ export default { throw new PermissionError(); } }, - timeFrames: (_: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) => { + timeFrames: (_: any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.ReadTimeFrame)) { return dataSources.lendingStationAPI.timeFrames(offset, limit); } else { @@ -99,19 +99,15 @@ export default { isLockedByMe: (parent: any, __: any, { req }: { req: any }) => isLockedByMe(parent, { req }), isLocked: (parent: any, __: any, { req }: { req: any }) => isLocked(parent, { req }) }, - LoanPeriod: { - loanTimes (parent: any) { - return parent.loanTimes ? parent.loanTimes : []; + DateRange: { + from (parent: string) { + return parent.replace(/^\[(.*),.*\)$/, '$1'); + }, + to (parent: string) { + return parent.replace(/^\[.*,(.*)\)$/, '$1'); } }, TimeFrame: { - from (parent: any) { - return (parent.dateRange as string).split(',')[0].replace('[', ''); - }, - to (parent: any) { - const str = (parent.dateRange as string).split(',')[1].replace(')', ''); - return (str.length > 0) ? str : null; - }, cargoBike (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { if (req.permissions.includes(Permission.ReadBike)) { return dataSources.cargoBikeAPI.cargoBikeByTimeFrameId(parent.id); diff --git a/src/resolvers/participantResolvers.ts b/src/resolvers/participantResolvers.ts index e636dae..456ee12 100644 --- a/src/resolvers/participantResolvers.ts +++ b/src/resolvers/participantResolvers.ts @@ -30,7 +30,7 @@ export default { throw new PermissionError(); } }, - participants: (_: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) => { + participants: (_: any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.ReadParticipant)) { return dataSources.participantAPI.getParticipants(offset, limit); } else { @@ -44,7 +44,7 @@ export default { throw new PermissionError(); } }, - engagements: (_: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) => { + engagements: (_: any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.ReadEngagement)) { return dataSources.participantAPI.engagements(offset, limit); } else { @@ -58,7 +58,7 @@ export default { throw new PermissionError(); } }, - engagementTypes: (_: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) => { + engagementTypes: (_: any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.ReadEngagement)) { return dataSources.participantAPI.engagementTypes(offset, limit); } else { @@ -113,13 +113,6 @@ export default { throw new PermissionError(); } }, - from (parent: any) { - return (parent.dateRange as string).split(',')[0].replace('[', ''); - }, - to (parent: any) { - const str = (parent.dateRange as string).split(',')[1].replace(')', ''); - return (str.length > 0) ? str : null; - }, isLockedByMe: (parent: any, __: any, { req }: { req: any }) => isLockedByMe(parent, { req }), isLocked: (parent: any, __: any, { req }: { req: any }) => isLocked(parent, { req }) }, diff --git a/src/resolvers/providerResolvers.ts b/src/resolvers/providerResolvers.ts index 5296c75..ba182a7 100644 --- a/src/resolvers/providerResolvers.ts +++ b/src/resolvers/providerResolvers.ts @@ -23,7 +23,7 @@ import { PermissionError } from '../errors/PermissionError'; export default { Query: { - providers: (_: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) => { + providers: (_: any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.ReadProvider)) { return dataSources.providerAPI.provider(offset, limit); } else { @@ -37,7 +37,7 @@ export default { throw new PermissionError(); } }, - organisations: (_: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) => { + organisations: (_: any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.ReadOrganisation)) { return dataSources.providerAPI.organisations(offset, limit); } else { diff --git a/src/resolvers/workshopResolvers.ts b/src/resolvers/workshopResolvers.ts index 295cd76..82fbd33 100644 --- a/src/resolvers/workshopResolvers.ts +++ b/src/resolvers/workshopResolvers.ts @@ -31,7 +31,7 @@ export default { throw new PermissionError(); } }, - workshopTypes: (_: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) => { + workshopTypes: (_: any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.ReadWorkshop)) { return dataSources.workshopAPI.workshopTypes(offset, limit); } else { @@ -45,7 +45,7 @@ export default { throw new PermissionError(); } }, - workshops: (_: any, { offset, limit }: { offset: number, limit: number }, { dataSources, req }: { dataSources: any, req: any }) => { + workshops: (_: any, { offset, limit }: { offset?: number, limit?: number }, { dataSources, req }: { dataSources: any, req: any }) => { if (req.permissions.includes(Permission.ReadWorkshop)) { return dataSources.workshopAPI.workshops(offset, limit); } else { diff --git a/src/schema/type-defs.ts b/src/schema/type-defs.ts index 9bd7ad0..b55e6a8 100644 --- a/src/schema/type-defs.ts +++ b/src/schema/type-defs.ts @@ -21,27 +21,38 @@ import { gql } from 'apollo-server-express'; export default gql` - "timestamp object YYYY-MM-ddThh:mm:ss.sssZ" + "date object YYYY-MM-dd" scalar Date + "timestamp object YYYY-MM-ddThh:mm:ss.sssZ" + scalar DateTime "only time hh-mm-ss" scalar Time - + """ + is of american format [-]$[0-9]+.[0-9][0-9] + commas every three digits and . for decimals with 2 digits after the . + There can be a leading -. + There is a currency signe at the first position or second position if - is set. + The kind of currency depends on the database. + """ + scalar Money + "The CargoBike type is central to the graph. You could call it the root." type CargoBike { id: ID! "see column A in info tabelle" - group: Group - name: String + group: Group! + name: String! + state: BikeState modelName: String numberOfWheels: Int forCargo: Boolean forChildren: Boolean - numberOfChildren: Int! + numberOfChildren: Int """ Safety is a custom type, that stores information about security features. TODO: Should this be called Security? """ - security: Security! + security: Security """ Does not refer to an extra table in the database. """ @@ -49,9 +60,11 @@ export default gql` """ Does not refer to an extra table in the database. """ - dimensionsAndLoad: DimensionsAndLoad! + dimensionsAndLoad: DimensionsAndLoad + "If offset or limit is not provided, both values are ignored" bikeEvents(offset: Int, limit: Int): [BikeEvent] - equipment(offset: Int!, limit: Int!): [Equipment] + "If offset or limit is not provided, both values are ignored" + equipment(offset: Int, limit: Int): [Equipment] "Refers to equipment that is not unique. See kommentierte info tabelle -> Fragen -> Frage 2" equipmentType: [EquipmentType] "Sticker State" @@ -60,19 +73,29 @@ export default gql` provider: Provider "all participants currently engaged with the cargoBike" participants: [Participant] - insuranceData: InsuranceData! + insuranceData: InsuranceData lendingStation: LendingStation taxes: Taxes currentEngagements: [Engagement] - engagement(offset: Int!, limit: Int!): [Engagement] + "If offset or limit is not provided, both values are ignored" + engagement(offset: Int, limit: Int): [Engagement] timeFrames: [TimeFrame] isLocked: Boolean! isLockedByMe: Boolean! "null if not locked by other user" lockedBy: ID - lockedUntil: Date + lockedUntil: DateTime } + """ + Status of the CargoBike. More fields will be added, or removed. + """ + enum BikeState { + ACTIVE + INACTIVE + INPREPARATION + } + """ if you want to add bike to a lending station, create a new timeFrame with to: Date = null """ @@ -80,42 +103,52 @@ export default gql` "see column A in info tabelle" group: Group! name: String! - modelName: String! - numberOfWheels: Int! - forCargo: Boolean! - forChildren: Boolean! - numberOfChildren: Int! + state: BikeState + modelName: String + numberOfWheels: Int + forCargo: Boolean + forChildren: Boolean + numberOfChildren: Int """ Safety is a custom type, that stores information about security features. - TODO: Should this be called Security? """ - security: SecurityCreateInput! + security: SecurityCreateInput """ Does not refer to an extra table in the database. """ - technicalEquipment: TechnicalEquipmentCreateInput! + technicalEquipment: TechnicalEquipmentCreateInput """ Does not refer to an extra table in the database. """ - dimensionsAndLoad: DimensionsAndLoadCreateInput! - "Refers to equipment that is not unique. See kommentierte info tabelle -> Fragen -> Frage 2" + dimensionsAndLoad: DimensionsAndLoadCreateInput + """ + Refers to equipment that is not unique. See kommentierte info tabelle -> Fragen -> Frage 2 + When set to null or [], no relations will be added. + """ equipmentTypeIds: [ID] + """ + Refers to unique equipment + When set to null or [], no relations will be added. + When specified id is in a relation with another bike, this relation will be deleted. + """ + equipmentIds: [ID] "Sticker State" stickerBikeNameState: StickerBikeNameState note: String providerId: ID - insuranceData: InsuranceDataCreateInput! - taxes: TaxesCreateInput! + insuranceData: InsuranceDataCreateInput + taxes: TaxesCreateInput } """ - if you want to add bike to a lending station, create a new timeFrame with to: Date = null + If you want to add bike to a lending station, create a new timeFrame with to: Date = null """ input CargoBikeUpdateInput { id: ID! "see column A in info tabelle" group: Group name: String + state: BikeState modelName: String numberOfWheels: Int forCargo: Boolean @@ -123,7 +156,6 @@ export default gql` numberOfChildren: Int """ Safety is a custom type, that stores information about security features. - TODO: Should this be called Security? """ security: SecurityUpdateInput """ @@ -136,9 +168,19 @@ export default gql` dimensionsAndLoad: DimensionsAndLoadUpdateInput """ Refers to equipment that is not unique. See kommentierte info tabelle -> Fragen -> Frage 2 - If set, ols relations will be over written. Set [] to delete all + When set to null, field will be ignored. + When set to [], all relations will be deleted. + Else all realtions will be deleted and the specified relations will be added. """ equipmentTypeIds: [ID] + """ + Refers to unique equipment + When set to null, field will be ignored. + When set to [], all relations will be deleted. + Else all realtions will be deleted and the specified relations will be added. + When specified id is in a relation with another bike, this relation will be deleted. + """ + equipmentIds: [ID] "Sticker State" stickerBikeNameState: StickerBikeNameState note: String @@ -153,15 +195,15 @@ export default gql` """ Eventually, this field will become an enum or a separate data table and user can choose from a pool of insurance companies. """ - name: String! - benefactor: String! - billing: String! - noPnP: String! + name: String + benefactor: String + billing: String + noPnP: String "eg. Anbieter, flotte, eigenleistung" - maintenanceResponsible: String! - maintenanceBenefactor: String! + maintenanceResponsible: String + maintenanceBenefactor: String maintenanceAgreement: String - hasFixedRate: Boolean! + hasFixedRate: Boolean fixedRate: Float """ Projektzuschuss: @@ -171,7 +213,7 @@ export default gql` There is a currency signe at the first position or second position if - is set. The kind of currency depends on the database. """ - projectAllowance: String + projectAllowance: Money notes: String } @@ -179,15 +221,15 @@ export default gql` """ Eventually, this field will become an enum or a separate data table and user can choose from a pool of insurance companies. """ - name: String! - benefactor: String! - billing: String! - noPnP: String! + name: String + benefactor: String + billing: String + noPnP: String "eg. Anbieter, flotte, eigenleistung" - maintenanceResponsible: String! - maintenanceBenefactor: String! + maintenanceResponsible: String + maintenanceBenefactor: String maintenanceAgreement: String - hasFixedRate: Boolean! + hasFixedRate: Boolean fixedRate: Float """ Projektzuschuss: @@ -197,7 +239,7 @@ export default gql` You can pass a currency signe at the first position or second position of + or - is set. The kind of currency depends on the database. """ - projectAllowance: String + projectAllowance: Money notes: String } @@ -223,37 +265,51 @@ export default gql` You can pass a currency signe at the first position or second position of + or - is set. The kind of currency depends on the database. """ - projectAllowance: String + projectAllowance: Money notes: String } + type NumRange { + min: Float + max: Float + } + + """ + If min or max is omitted, the omitted value will be the same as the other given value + So if you pass one as null, both values with be over written with null. + """ + input NumRangeInput { + min: Float + max: Float + } + "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! + hasCoverBox: Boolean "cover box can be locked" - lockable: Boolean! - boxLength: Float! - boxWidth: Float! - boxHeight: Float! - maxWeightBox: Float! - maxWeightLuggageRack: Float! - maxWeightTotal: Float! - bikeLength: Float! + lockable: Boolean + boxLengthRange: NumRange + boxWidthRange: NumRange + boxHeightRange: NumRange + 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! + hasCoverBox: Boolean + lockable: Boolean + boxLengthRange: NumRangeInput + boxWidthRange: NumRangeInput + boxHeightRange: NumRangeInput + maxWeightBox: Float + maxWeightLuggageRack: Float + maxWeightTotal: Float + bikeLength: Float bikeWidth: Float bikeHeight: Float bikeWeight: Float @@ -262,9 +318,9 @@ export default gql` input DimensionsAndLoadUpdateInput { hasCoverBox: Boolean lockable: Boolean - boxLength: Float - boxWidth: Float - boxHeight: Float + boxLengthRange: NumRangeInput + boxWidthRange: NumRangeInput + boxHeightRange: NumRangeInput maxWeightBox: Float maxWeightLuggageRack: Float maxWeightTotal: Float @@ -280,16 +336,16 @@ export default gql` So no id needed for mutation. One Mutation for the CargoBike will be enough. """ type TechnicalEquipment { - bicycleShift: String! - isEBike: Boolean! - hasLightSystem: Boolean! + bicycleShift: String + isEBike: Boolean + hasLightSystem: Boolean specialFeatures: String } input TechnicalEquipmentCreateInput { - bicycleShift: String! - isEBike: Boolean! - hasLightSystem: Boolean! + bicycleShift: String + isEBike: Boolean + hasLightSystem: Boolean specialFeatures: String } @@ -306,7 +362,7 @@ export default gql` So no id needed for mutation. One Mutation for the CargoBike will be enough. """ type Security { - frameNumber: String! + frameNumber: String keyNumberFrameLock: String keyNumberAXAChain: String policeCoding: String @@ -314,7 +370,7 @@ export default gql` } input SecurityCreateInput { - frameNumber: String! + frameNumber: String keyNumberFrameLock: String keyNumberAXAChain: String policeCoding: String @@ -354,8 +410,7 @@ export default gql` """ type Participant { id: ID! - start: Date! - end: Date + dateRange: DateRange! contactInformation: ContactInformation! usernamefLotte: String usernameSlack: String @@ -375,13 +430,12 @@ export default gql` isLockedByMe: Boolean! "null if not locked by other user" lockedBy: ID - lockedUntil: Date + lockedUntil: DateTime } input ParticipantCreateInput { "if not set, CURRENT_DATE will be used" - start: Date - end: Date + dateRange: DateRangeInput! "must create contactinformation first, if you want to use new" contactInformationId: ID! usernamefLotte: String @@ -396,9 +450,7 @@ export default gql` input ParticipantUpdateInput { id: ID! - "if not set, CURRENT_DATE will be used" - start: Date - end: Date + dateRange: DateRangeInput "must create contactinformation first, if you want to use new" contactInformationId: ID usernamefLotte: String @@ -428,7 +480,7 @@ export default gql` isLockedByMe: Boolean! "null if not locked by other user" lockedBy: ID - lockedUntil: Date + lockedUntil: DateTime } input WorkshopCreateInput { @@ -458,7 +510,7 @@ export default gql` isLockedByMe: Boolean! "null if not locked by other user" lockedBy: ID - lockedUntil: Date + lockedUntil: DateTime } input WorkshopTypeCreateInput { @@ -478,7 +530,7 @@ export default gql` isLockedByMe: Boolean! "null if not locked by other user" lockedBy: ID - lockedUntil: Date + lockedUntil: DateTime } input EngagementTypeCreateInput { @@ -496,43 +548,38 @@ export default gql` type Engagement { id: ID! engagementType: EngagementType! - from: Date! - to: Date + dateRange: DateRange! participant: Participant! cargoBike: CargoBike! isLocked: Boolean! isLockedByMe: Boolean! "null if not locked by other user" lockedBy: ID - lockedUntil: Date + lockedUntil: DateTime } input EngagementCreateInput { engagementTypeId: ID! - "will use CURRENT_DATE if not set" - from: Date - "will use infinit if not set" - to: Date + dateRange: DateRangeInput! participantId: ID! cargoBikeId: ID! } input EngagementUpdateInput { id: ID! engagementTypeId: ID - from: Date - to: Date + dateRange: DateRangeInput participantId: ID cargoBikeId: ID keepLock: Boolean } type Taxes { - costCenter: String! + costCenter: String organisationArea: OrganisationArea } input TaxesCreateInput { - costCenter: String! + costCenter: String organisationArea: OrganisationArea } @@ -560,7 +607,7 @@ export default gql` isLockedByMe: Boolean! "null if not locked by other user" lockedBy: ID - lockedUntil: Date + lockedUntil: DateTime } input EquipmentCreateInput { @@ -592,11 +639,11 @@ export default gql` isLockedByMe: Boolean! "null if not locked by other user" lockedBy: ID - lockedUntil: Date + lockedUntil: DateTime } input EquipmentTypeCreateInput { - name: String + name: String! description: String } @@ -607,7 +654,7 @@ export default gql` keepLock: Boolean } - "An Event is a point in time, when the state of the bike somehow changed." + "An Event is a point in time concerning one cargo bike of an event type. For example a chain swap." type BikeEvent { id: ID! bikeEventType: BikeEventType! @@ -625,7 +672,7 @@ export default gql` isLockedByMe: Boolean! "null if not locked by other user" lockedBy: ID - lockedUntil: Date + lockedUntil: DateTime } input BikeEventCreateInput { @@ -663,7 +710,9 @@ export default gql` name: String! isLockedByMe: Boolean! isLocked: Boolean! - lockedUntil: Date + "null if not locked by other user" + lockedBy: ID + lockedUntil: DateTime } input BikeEventTypeUpdateInput { @@ -683,7 +732,7 @@ export default gql` isLockedByMe: Boolean! "null if not locked by other user" lockedBy: ID - lockedUntil: Date + lockedUntil: DateTime } "(dt. Anbieter)" @@ -717,7 +766,7 @@ export default gql` isLockedByMe: Boolean! "null if not locked by other user" lockedBy: ID - lockedUntil: Date + lockedUntil: DateTime } input PersonCreateInput { @@ -744,7 +793,7 @@ export default gql` isLockedByMe: Boolean! "null if not locked by other user" lockedBy: ID - lockedUntil: Date + lockedUntil: DateTime } input ContactInformationCreateInput { @@ -784,7 +833,7 @@ export default gql` isLockedByMe: Boolean! "null if not locked by other user" lockedBy: ID - lockedUntil: Date + lockedUntil: DateTime } input OrganisationCreateInput { @@ -828,7 +877,7 @@ export default gql` isLockedByMe: Boolean! "null if not locked by other user" lockedBy: ID - lockedUntil: Date + lockedUntil: DateTime } """ @@ -862,13 +911,13 @@ export default gql` """ type LoanPeriod { generalRemark: String - "notes for each day of the week, starting on Monday" - notes: [String] - """ - Loan times from and until for each day of the week. - Starting with Monday from, Monday to, Tuesday from, ..., Sunday to - """ - loanTimes: [String] + mo: String + tu: String + we: String + th: String + fr: String + sa: String + su: String } """ @@ -876,22 +925,35 @@ export default gql` """ input LoanPeriodInput { generalRemark: String - "notes for each day of the week, starting on Monday" - notes: [String!] + mo: String + tu: String + we: String + th: String + fr: String + sa: String + su: String + } + + type DateRange{ + from: Date! + "will be infinity of not omitted" + to: Date + } + + input DateRangeInput { + "format YYYY-MM-dd" + from: Date! """ - Loan times from and until for each day of the week. - Starting with Monday from, Monday to, Tuesday from, ..., Sunday to + format YYYY-MM-dd + will be infinity of not omitted """ - loanTimes: [String!] + to: Date } - + "(dt. Zeitscheibe) When was a bike where" type TimeFrame { id: ID! - "format YYYY-MM-dd" - from: Date! - "format YYYY-MM-dd" - to: Date + dateRange: DateRange! note: String lendingStation: LendingStation! cargoBike: CargoBike! @@ -899,12 +961,11 @@ export default gql` isLockedByMe: Boolean! "null if not locked by other user" lockedBy: ID - lockedUntil: Date + lockedUntil: DateTime } input TimeFrameCreateInput { - from: Date! - to: Date + dateRange: DateRangeInput! note: String lendingStationId: ID! cargoBikeId: ID! @@ -912,8 +973,7 @@ export default gql` input TimeFrameUpdateInput { id: ID! - from: Date - to: Date + dateRange: DateRangeInput note: String lendingStationId: ID cargoBikeId: ID @@ -953,41 +1013,55 @@ export default gql` type Query { "Will (eventually) return all properties of cargo bike" cargoBikeById(id:ID!): CargoBike - "returns cargoBikes ordered by name ascending, relations are not loaded, use cargoBikeById instead" - cargoBikes(offset: Int!, limit: Int!): [CargoBike!]! + "Returns cargoBikes ordered by name ascending. If offset or limit is not provided, both values are ignored." + cargoBikes(offset: Int, limit: Int): [CargoBike!]! engagementById(id: ID!): Engagement - engagements(offset: Int!, limit: Int!): [Engagement!]! + "If offset or limit is not provided, both values are ignored" + engagements(offset: Int, limit: Int): [Engagement!]! engagementTypeById(id: ID!): EngagementType - engagementTypes(offset: Int!, limit: Int!): [EngagementType!]! + "If offset or limit is not provided, both values are ignored" + engagementTypes(offset: Int, limit: Int): [EngagementType!]! "equipment by id, will return null if id not found" equipmentById(id: ID!): Equipment - equipment(offset: Int!, limit: Int!): [Equipment!]! + "If offset or limit is not provided, both values are ignored" + equipment(offset: Int, limit: Int): [Equipment!]! equipmentTypeById(id: ID!): EquipmentType - equipmentTypes(offset: Int!, limit: Int!): [EquipmentType!]! + "If offset or limit is not provided, both values are ignored" + equipmentTypes(offset: Int, limit: Int): [EquipmentType!]! "return null if id not found" providerById(id:ID!): Provider - "Returns providers with pagination" - providers(offset: Int!, limit: Int!): [Provider!]! + "Returns providers with pagination. If offset or limit is not provided, both values are ignored" + providers(offset: Int, limit: Int): [Provider!]! "participant by id" participantById(id:ID!): Participant - participants(offset: Int!, limit: Int!): [Participant!]! + "If offset or limit is not provided, both values are ignored" + participants(offset: Int, limit: Int): [Participant!]! workshopTypeById(id: ID!): WorkshopType - workshopTypes(offset: Int!, limit: Int!): [WorkshopType!]! + "If offset or limit is not provided, both values are ignored" + workshopTypes(offset: Int, limit: Int): [WorkshopType!]! workshopById(id: ID!): Workshop - workshops(offset: Int!, limit: Int!): [Workshop!]! + "If offset or limit is not provided, both values are ignored" + workshops(offset: Int, limit: Int): [Workshop!]! lendingStationById(id:ID!): LendingStation - lendingStations(offset: Int!, limit: Int!): [LendingStation!]! + "If offset or limit is not provided, both values are ignored" + lendingStations(offset: Int, limit: Int): [LendingStation!]! organisationById(id: ID!): Organisation - organisations(offset: Int!, limit: Int!): [Organisation!]! + "If offset or limit is not provided, both values are ignored" + organisations(offset: Int, limit: Int): [Organisation!]! timeFrameById(id: ID!): TimeFrame - timeFrames(offset: Int!, limit: Int!): [TimeFrame!]! + "If offset or limit is not provided, both values are ignored" + timeFrames(offset: Int, limit: Int): [TimeFrame!]! contactInformationById(id: ID!): ContactInformation - contactInformation(offset: Int!, limit: Int!): [ContactInformation!]! + "If offset or limit is not provided, both values are ignored" + contactInformation(offset: Int, limit: Int): [ContactInformation!]! personById(id: ID!): Person - persons(offset: Int!, limit: Int!): [Person!] - bikeEventTypes(offset: Int!, limit: Int!): [BikeEventType!] + "If offset or limit is not provided, both values are ignored" + persons(offset: Int, limit: Int): [Person!] + "If offset or limit is not provided, both values are ignored" + bikeEventTypes(offset: Int, limit: Int): [BikeEventType!] bikeEventTypeByd(id: ID!): BikeEventType - bikeEvents(offset: Int!, limit: Int!): [BikeEvent!]! + "If offset or limit is not provided, both values are ignored" + bikeEvents(offset: Int, limit: Int): [BikeEvent!]! bikeEventById(id:ID!): BikeEvent "actionLog for current user" actionLog: [ActionLog!] @@ -1062,23 +1136,23 @@ export default gql` """ createParticipant(participant: ParticipantCreateInput!): Participant! lockParticipant(id: ID!): Participant! - unlockParticipant(id: ID!): Boolean + unlockParticipant(id: ID!): Participant updateParticipant(participant: ParticipantUpdateInput!): Participant! deleteParticipant(id: ID!): Boolean! createWorkshopType(workshopType: WorkshopTypeCreateInput!): WorkshopType! lockWorkshopType(id: ID!): WorkshopType! - unlockWorkshopType(id: ID!): Boolean! + unlockWorkshopType(id: ID!): WorkshopType! updateWorkshopType(workshopType: WorkshopTypeUpdateInput!): WorkshopType! deleteWorkshopType(id: ID!): Boolean! createWorkshop(workshop: WorkshopCreateInput!): Workshop! lockWorkshop(id: ID!): Workshop! - unlockWorkshop(id: ID!): Boolean! + unlockWorkshop(id: ID!): Workshop! updateWorkshop(workshop: WorkshopUpdateInput!): Workshop! deleteWorkshop(id: ID!): Boolean! "create new contactInfo" createContactInformation(contactInformation: ContactInformationCreateInput!): ContactInformation! lockContactInformation(id: ID!): ContactInformation! - unlockContactInformation(id: ID!): Boolean! + unlockContactInformation(id: ID!): ContactInformation! updateContactInformation(contactInformation: ContactInformationUpdateInput!): ContactInformation! deleteContactInformation(id: ID!): Boolean! createPerson(person: PersonCreateInput!): Person! @@ -1089,22 +1163,22 @@ export default gql` "create Engagement" createEngagement(engagement: EngagementCreateInput): Engagement! lockEngagement(id: ID!): Engagement! - unlockEngagement(id: ID!): Boolean! + unlockEngagement(id: ID!): Engagement! updateEngagement(engagement: EngagementUpdateInput!): Engagement! deleteEngagement(id: ID!): Boolean! createEngagementType(engagementType: EngagementTypeCreateInput!): EngagementType! lockEngagementType(id: ID!): EngagementType! - unlockEngagementType(id: ID!): Boolean! + unlockEngagementType(id: ID!): EngagementType! updateEngagementType(engagementType: EngagementTypeUpdateInput!): EngagementType! deleteEngagementType(id: ID!): Boolean! createProvider(provider: ProviderCreateInput!): Provider! lockProvider(id: ID!): Provider! - unlockProvider(id: ID!): Boolean! + unlockProvider(id: ID!): Provider! updateProvider(provider: ProviderUpdateInput!): Provider! deleteProvider(id: ID!): Boolean! createOrganisation(organisation: OrganisationCreateInput!): Organisation! lockOrganisation(id: ID!): Organisation! - unlockOrganisation(id: ID!): Boolean! + unlockOrganisation(id: ID!): Organisation! updateOrganisation(organisation: OrganisationUpdateInput!): Organisation! deleteOrganisation(id: ID!): Boolean! }