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/lendingstationAPI.ts b/src/datasources/db/lendingstationAPI.ts index a0f84c6..52c08ad 100644 --- a/src/datasources/db/lendingstationAPI.ts +++ b/src/datasources/db/lendingstationAPI.ts @@ -183,10 +183,7 @@ export class LendingStationAPI extends DataSource { 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') diff --git a/src/datasources/db/utils.ts b/src/datasources/db/utils.ts index be8fbf1..7262417 100644 --- a/src/datasources/db/utils.ts +++ b/src/datasources/db/utils.ts @@ -23,45 +23,48 @@ 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 prepares the cargoBike struct, to be used in an update or create. + * 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 from - * @param to + * @param range */ -function genNumRange (from: number, to: number) { - if (from === null || from === undefined) { - from = to; - } else if (to === null || to === undefined) { - to = from; +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; } - return from ? '[' + from + ',' + to + ']' : null; + 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) { - cargoBike.dimensionsAndLoad.boxLengthRange = genNumRange(cargoBike.dimensionsAndLoad.minBoxLength, cargoBike.dimensionsAndLoad.maxBoxLength); - cargoBike.dimensionsAndLoad.boxWidthRange = genNumRange(cargoBike.dimensionsAndLoad.minBoxWidth, cargoBike.dimensionsAndLoad.maxBoxWidth); - cargoBike.dimensionsAndLoad.boxHeightRange = genNumRange(cargoBike.dimensionsAndLoad.minBoxHeight, cargoBike.dimensionsAndLoad.maxBoxHeight); - // delete this so update cargo bike works - delete cargoBike.dimensionsAndLoad.minBoxLength; - delete cargoBike.dimensionsAndLoad.maxBoxLength; - delete cargoBike.dimensionsAndLoad.minBoxWidth; - delete cargoBike.dimensionsAndLoad.maxBoxWidth; - delete cargoBike.dimensionsAndLoad.minBoxHeight; - delete cargoBike.dimensionsAndLoad.maxBoxHeight; + 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. * Returns true if locked by other user. @@ -273,10 +276,10 @@ 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; diff --git a/src/model/CargoBike.ts b/src/model/CargoBike.ts index 4193a41..965651a 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'; @@ -90,13 +89,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,10 +111,14 @@ export class TechnicalEquipment { } export class DimensionsAndLoad { - @Column() + @Column({ + nullable: true + }) hasCoverBox: boolean; - @Column() + @Column({ + nullable: true + }) lockable:boolean; @Column({ @@ -156,7 +165,7 @@ export class DimensionsAndLoad { @Column({ nullable: true, - type: 'numrange' + type: 'decimal' }) bikeWidth: number; @@ -195,6 +204,11 @@ export class CargoBike implements Lockable { }) name: string; + @Column({ + nullable: true + }) + state: string; + @OneToMany(type => Equipment, equipment => equipment.cargoBikeId, { nullable: true, eager: true 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/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/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/resolvers/cargoBikeResolver.ts b/src/resolvers/cargoBikeResolver.ts index 502f181..b90b372 100644 --- a/src/resolvers/cargoBikeResolver.ts +++ b/src/resolvers/cargoBikeResolver.ts @@ -161,45 +161,12 @@ export default { isLockedByMe: (parent: any, __: any, { req }: { req: any }) => isLockedByMe(parent, { req }), isLocked: (parent: any, __: any, { req }: { req: any }) => isLocked(parent, { req }) }, - DimensionsAndLoad: { - minBoxLength: (parent: any) => { - if (!parent.boxLengthRange || parent.boxLengthRange === 'empty') { - return null; - } - return parent.boxLengthRange ? (parent.boxLengthRange as string).split(',')[0].replace('[', '') : null; - }, - maxBoxLength: (parent: any) => { - if (!parent.boxLengthRange || parent.boxLengthRange === 'empty') { - return null; - } - const str = (parent.boxLengthRange as string).split(',')[1].replace(']', ''); - return (str.length > 0) ? str : null; - }, - minBoxWidth: (parent: any) => { - if (!parent.boxWidthRange || parent.boxWidthRange === 'empty') { - return null; - } - return parent.boxWidthRange ? (parent.boxWidthRange as string).split(',')[0].replace('[', '') : null; + NumRange: { + min: (parent: string) => { + return parent.replace(/^\[(.*),.*]$/, '$1'); }, - maxBoxWidth: (parent: any) => { - if (!parent.boxWidthRange || parent.boxWidthRange === 'empty') { - return null; - } - const str = (parent.boxWidthRange as string).split(',')[1].replace(']', ''); - return (str.length > 0) ? str : null; - }, - minBoxHeight: (parent: any) => { - if (!parent.boxHeightRange || parent.boxHeightRange === 'empty') { - return null; - } - return parent.boxHeightRange ? (parent.boxHeightRange as string).split(',')[0].replace('[', '') : null; - }, - maxBoxHeight: (parent: any) => { - if (!parent.boxHeightRange || parent.boxHeightRange === 'empty') { - return null; - } - const str = (parent.boxHeightRange as string).split(',')[1].replace(']', ''); - return (str.length > 0) ? str : null; + max: (parent: string) => { + return parent.replace(/^\[.*,(.*)]$/, '$1'); } }, Equipment: { diff --git a/src/resolvers/lendingStationResolvers.ts b/src/resolvers/lendingStationResolvers.ts index eb26f62..91d25bb 100644 --- a/src/resolvers/lendingStationResolvers.ts +++ b/src/resolvers/lendingStationResolvers.ts @@ -104,14 +104,15 @@ export default { return parent.loanTimes ? parent.loanTimes : []; } }, - 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; + DateRange: { + from (parent: string) { + return parent.replace(/^\[(.*),.*\)$/, '$1'); }, + to (parent: string) { + return parent.replace(/^\[.*,(.*)\)$/, '$1'); + } + }, + TimeFrame: { 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/schema/type-defs.ts b/src/schema/type-defs.ts index d92729d..af19618 100644 --- a/src/schema/type-defs.ts +++ b/src/schema/type-defs.ts @@ -33,12 +33,14 @@ export default gql` 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 + name: String! + state: BikeState modelName: String numberOfWheels: Int forCargo: Boolean @@ -56,7 +58,7 @@ 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] "If offset or limit is not provided, both values are ignored" @@ -69,7 +71,7 @@ export default gql` provider: Provider "all participants currently engaged with the cargoBike" participants: [Participant] - insuranceData: InsuranceData! + insuranceData: InsuranceData lendingStation: LendingStation taxes: Taxes currentEngagements: [Engagement] @@ -83,6 +85,15 @@ export default gql` lockedUntil: Date } + """ + 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 """ @@ -90,6 +101,7 @@ export default gql` "see column A in info tabelle" group: Group! name: String! + state: BikeState modelName: String! numberOfWheels: Int! forCargo: Boolean! @@ -102,11 +114,11 @@ export default gql` """ 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! + 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. @@ -122,8 +134,8 @@ export default gql` stickerBikeNameState: StickerBikeNameState note: String providerId: ID - insuranceData: InsuranceDataCreateInput! - taxes: TaxesCreateInput! + insuranceData: InsuranceDataCreateInput + taxes: TaxesCreateInput } """ @@ -134,6 +146,7 @@ export default gql` "see column A in info tabelle" group: Group name: String + state: BikeState modelName: String numberOfWheels: Int forCargo: Boolean @@ -180,15 +193,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: @@ -206,15 +219,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: @@ -254,17 +267,28 @@ export default gql` 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! - minBoxLength: Float - maxBoxLength: Float - minBoxWidth: Float - maxBoxWidth: Float - minBoxHeight: Float - maxBoxHeight: Float + lockable: Boolean + boxLengthRange: NumRange + boxWidthRange: NumRange + boxHeightRange: NumRange maxWeightBox: Float maxWeightLuggageRack: Float maxWeightTotal: Float @@ -275,14 +299,11 @@ export default gql` } input DimensionsAndLoadCreateInput { - hasCoverBox: Boolean! - lockable: Boolean! - minBoxLength: Float - maxBoxLength: Float - minBoxWidth: Float - maxBoxWidth: Float - minBoxHeight: Float - maxBoxHeight: Float + hasCoverBox: Boolean + lockable: Boolean + boxLengthRange: NumRangeInput + boxWidthRange: NumRangeInput + boxHeightRange: NumRangeInput maxWeightBox: Float maxWeightLuggageRack: Float maxWeightTotal: Float @@ -295,12 +316,9 @@ export default gql` input DimensionsAndLoadUpdateInput { hasCoverBox: Boolean lockable: Boolean - minBoxLength: Float - maxBoxLength: Float - minBoxWidth: Float - maxBoxWidth: Float - minBoxHeight: Float - maxBoxHeight: Float + boxLengthRange: NumRangeInput + boxWidthRange: NumRangeInput + boxHeightRange: NumRangeInput maxWeightBox: Float maxWeightLuggageRack: Float maxWeightTotal: Float @@ -316,16 +334,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 } @@ -563,12 +581,12 @@ export default gql` } type Taxes { - costCenter: String! + costCenter: String organisationArea: OrganisationArea } input TaxesCreateInput { - costCenter: String! + costCenter: String organisationArea: OrganisationArea } @@ -643,7 +661,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! @@ -923,13 +941,26 @@ export default gql` loanTimes: [String!] } - "(dt. Zeitscheibe) When was a bike where" - type TimeFrame { - id: ID! - "format YYYY-MM-dd" + type DateRange{ from: Date! + "will be infinity of not omitted" + to: Date + } + + input DateRangeInput { "format YYYY-MM-dd" + from: Date! + """ + format YYYY-MM-dd + will be infinity of not omitted + """ to: Date + } + + "(dt. Zeitscheibe) When was a bike where" + type TimeFrame { + id: ID! + dateRange: DateRange! note: String lendingStation: LendingStation! cargoBike: CargoBike! @@ -941,8 +972,7 @@ export default gql` } input TimeFrameCreateInput { - from: Date! - to: Date + dateRange: DateRangeInput! note: String lendingStationId: ID! cargoBikeId: ID! @@ -950,8 +980,7 @@ export default gql` input TimeFrameUpdateInput { id: ID! - from: Date - to: Date + dateRange: DateRangeInput note: String lendingStationId: ID cargoBikeId: ID