Merge pull request #23 from fLotte-meets-HWR-DB/dev

Dev
pull/26/head
leonnicolas 4 years ago committed by GitHub
commit 69d44b792a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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) [![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 ## 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 ## Usage
### Docker ### Docker
You can build and run a docker image with
```bash ```bash
docker build -t <image name> . docker build -t <image name> .
docker run --rm -p 4000:4000 <image name> docker run --rm -p 4000:4000 <image name> -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 Install gulp if not installed
```bash ```bash
npm -g gulp npm -g gulp
@ -21,25 +23,28 @@ npm install
gulp gulp
npm start npm start
``` ```
You can set the [environment variables](#Environment-Variables) in a _.env_ file.
### For Development ### For Development
Install node\_modules and gulp Install node\_modules and gulp
```bash ```bash
npm -g gulp npm -g gulp
npm install npm install
``` ```
And start gulp in watch mode Start gulp in watch mode to recompile the type script
```bash ```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 ## Environment Variables
The following environment variables can be used to configure the server: The following environment variables can be used to configure the server:
```bash ```bash
RPC_HOST=host:port RPC_HOST=host:port
NODE_ENV=development/porduction NODE_ENV=develop/production
POSTGRES_CONNECTION_URL=postgres://username:password@host:port/database_name 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 - __NODE_ENV__ will not check authentication if set to development
- __POSTGRES_CONNECTION_URL__ for connection with the postgres database - __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.

@ -183,10 +183,7 @@ export class LendingStationAPI extends DataSource {
async createTimeFrame (timeFrame: any) { async createTimeFrame (timeFrame: any) {
return await this.connection.transaction(async (entityManager: EntityManager) => { return await this.connection.transaction(async (entityManager: EntityManager) => {
if (timeFrame.to === undefined) { genDateRange(timeFrame);
timeFrame.to = '';
}
timeFrame.dateRange = '[' + timeFrame.from + ',' + timeFrame.to + ')';
// checking for overlapping time frames // checking for overlapping time frames
const overlapping = await entityManager.getRepository(TimeFrame) const overlapping = await entityManager.getRepository(TimeFrame)
.createQueryBuilder('timeframe') .createQueryBuilder('timeframe')

@ -23,45 +23,48 @@ import { ActionLog, Actions } from '../../model/ActionLog';
import { UserInputError } from 'apollo-server-express'; import { UserInputError } from 'apollo-server-express';
export function genDateRange (struct: any) { export function genDateRange (struct: any) {
if (struct.to === undefined) { if (!struct.dateRange || !struct.dateRange.from) {
struct.to = '';
}
struct.dateRange = '[' + struct.from + ',' + struct.to + ')';
if (struct.from === undefined) {
delete struct.dateRange; 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 struct.dateRange = `[${struct.dateRange.from},${struct.dateRange.to})`;
delete struct.from;
delete struct.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. * It creates the numrange attributes than can be understood by postgres.
* @param from * @param range
* @param to
*/ */
function genNumRange (from: number, to: number) { function genNumRange (range: { min: number, max: number}) :string {
if (from === null || from === undefined) { if (!range || (!range.max && !range.min)) {
from = to; return null;
} else if (to === null || to === undefined) { } else if (range.min === null || range.min === undefined) {
to = from; 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) { export function genBoxDimensions (cargoBike: any) {
cargoBike.dimensionsAndLoad.boxLengthRange = genNumRange(cargoBike.dimensionsAndLoad.minBoxLength, cargoBike.dimensionsAndLoad.maxBoxLength); if (!cargoBike.dimensionsAndLoad) { return; }
cargoBike.dimensionsAndLoad.boxWidthRange = genNumRange(cargoBike.dimensionsAndLoad.minBoxWidth, cargoBike.dimensionsAndLoad.maxBoxWidth); cargoBike.dimensionsAndLoad.boxLengthRange = genNumRange(cargoBike.dimensionsAndLoad.boxLengthRange);
cargoBike.dimensionsAndLoad.boxHeightRange = genNumRange(cargoBike.dimensionsAndLoad.minBoxHeight, cargoBike.dimensionsAndLoad.maxBoxHeight); cargoBike.dimensionsAndLoad.boxWidthRange = genNumRange(cargoBike.dimensionsAndLoad.boxWidthRange);
// delete this so update cargo bike works cargoBike.dimensionsAndLoad.boxHeightRange = genNumRange(cargoBike.dimensionsAndLoad.boxHeightRange);
delete cargoBike.dimensionsAndLoad.minBoxLength;
delete cargoBike.dimensionsAndLoad.maxBoxLength;
delete cargoBike.dimensionsAndLoad.minBoxWidth;
delete cargoBike.dimensionsAndLoad.maxBoxWidth;
delete cargoBike.dimensionsAndLoad.minBoxHeight;
delete cargoBike.dimensionsAndLoad.maxBoxHeight;
} }
/** /**
* 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. * 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 // 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])) { if (updates[value] && typeof updates[value] === 'object' && !Array.isArray(updates[value])) {
Object.keys(updates[value]).forEach(subValue => { 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 { } else {
ret.push(alias + '."' + value + '"'); ret.push(`${alias}."${value}"`);
} }
}); });
return ret; return ret;

@ -30,7 +30,6 @@ import {
DeleteDateColumn DeleteDateColumn
} from 'typeorm'; } from 'typeorm';
import { Provider } from './Provider'; import { Provider } from './Provider';
import { Participant } from './Participant';
import { InsuranceData } from './InsuranceData'; import { InsuranceData } from './InsuranceData';
import { TimeFrame } from './TimeFrame'; import { TimeFrame } from './TimeFrame';
import { Taxes } from './Taxes'; import { Taxes } from './Taxes';
@ -90,13 +89,19 @@ export class Security {
adfcCoding: string; adfcCoding: string;
} }
export class TechnicalEquipment { export class TechnicalEquipment {
@Column() @Column({
nullable: true
})
bicycleShift: string; bicycleShift: string;
@Column() @Column({
nullable: true
})
isEBike: boolean; isEBike: boolean;
@Column() @Column({
nullable: true
})
hasLightSystem: boolean; hasLightSystem: boolean;
@Column({ @Column({
@ -106,10 +111,14 @@ export class TechnicalEquipment {
} }
export class DimensionsAndLoad { export class DimensionsAndLoad {
@Column() @Column({
nullable: true
})
hasCoverBox: boolean; hasCoverBox: boolean;
@Column() @Column({
nullable: true
})
lockable:boolean; lockable:boolean;
@Column({ @Column({
@ -156,7 +165,7 @@ export class DimensionsAndLoad {
@Column({ @Column({
nullable: true, nullable: true,
type: 'numrange' type: 'decimal'
}) })
bikeWidth: number; bikeWidth: number;
@ -195,6 +204,11 @@ export class CargoBike implements Lockable {
}) })
name: string; name: string;
@Column({
nullable: true
})
state: string;
@OneToMany(type => Equipment, equipment => equipment.cargoBikeId, { @OneToMany(type => Equipment, equipment => equipment.cargoBikeId, {
nullable: true, nullable: true,
eager: true eager: true

@ -42,9 +42,7 @@ export class ContactInformation implements Lockable {
}) })
participantId: number; participantId: number;
@Column(type => { @Column(type => { return Address; })
return Address;
})
address: Address; address: Address;
@Column({ @Column({

@ -20,22 +20,34 @@ This file is part of fLotte-API-Server.
import { Column } from 'typeorm'; import { Column } from 'typeorm';
export class InsuranceData { export class InsuranceData {
@Column() @Column({
nullable: true
})
name: string; name: string;
@Column() @Column({
nullable: true
})
benefactor: string; benefactor: string;
@Column() @Column({
nullable: true
})
billing: string; billing: string;
@Column() @Column({
nullable: true
})
noPnP: string; noPnP: string;
@Column() @Column({
nullable: true
})
maintenanceResponsible: string; maintenanceResponsible: string;
@Column() @Column({
nullable: true
})
maintenanceBenefactor: string; maintenanceBenefactor: string;
@Column({ @Column({

@ -25,7 +25,9 @@ export enum OrganisationArea {
ZB = 'ZB' ZB = 'ZB'
} }
export class Taxes { export class Taxes {
@Column() @Column({
nullable: true
})
costCenter: string; costCenter: string;
@Column({ @Column({

@ -161,45 +161,12 @@ export default {
isLockedByMe: (parent: any, __: any, { req }: { req: any }) => isLockedByMe(parent, { req }), isLockedByMe: (parent: any, __: any, { req }: { req: any }) => isLockedByMe(parent, { req }),
isLocked: (parent: any, __: any, { req }: { req: any }) => isLocked(parent, { req }) isLocked: (parent: any, __: any, { req }: { req: any }) => isLocked(parent, { req })
}, },
DimensionsAndLoad: { NumRange: {
minBoxLength: (parent: any) => { min: (parent: string) => {
if (!parent.boxLengthRange || parent.boxLengthRange === 'empty') { return parent.replace(/^\[(.*),.*]$/, '$1');
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;
}, },
maxBoxWidth: (parent: any) => { max: (parent: string) => {
if (!parent.boxWidthRange || parent.boxWidthRange === 'empty') { return parent.replace(/^\[.*,(.*)]$/, '$1');
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;
} }
}, },
Equipment: { Equipment: {

@ -104,14 +104,15 @@ export default {
return parent.loanTimes ? parent.loanTimes : []; return parent.loanTimes ? parent.loanTimes : [];
} }
}, },
TimeFrame: { DateRange: {
from (parent: any) { from (parent: string) {
return (parent.dateRange as string).split(',')[0].replace('[', ''); return parent.replace(/^\[(.*),.*\)$/, '$1');
}, },
to (parent: any) { to (parent: string) {
const str = (parent.dateRange as string).split(',')[1].replace(')', ''); return parent.replace(/^\[.*,(.*)\)$/, '$1');
return (str.length > 0) ? str : null; }
}, },
TimeFrame: {
cargoBike (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) { cargoBike (parent: any, __: any, { dataSources, req }: { dataSources: any, req: any }) {
if (req.permissions.includes(Permission.ReadBike)) { if (req.permissions.includes(Permission.ReadBike)) {
return dataSources.cargoBikeAPI.cargoBikeByTimeFrameId(parent.id); return dataSources.cargoBikeAPI.cargoBikeByTimeFrameId(parent.id);

@ -33,12 +33,14 @@ export default gql`
The kind of currency depends on the database. The kind of currency depends on the database.
""" """
scalar Money scalar Money
"The CargoBike type is central to the graph. You could call it the root." "The CargoBike type is central to the graph. You could call it the root."
type CargoBike { type CargoBike {
id: ID! id: ID!
"see column A in info tabelle" "see column A in info tabelle"
group: Group group: Group
name: String name: String!
state: BikeState
modelName: String modelName: String
numberOfWheels: Int numberOfWheels: Int
forCargo: Boolean forCargo: Boolean
@ -56,7 +58,7 @@ export default gql`
""" """
Does not refer to an extra table in the database. 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" "If offset or limit is not provided, both values are ignored"
bikeEvents(offset: Int, limit: Int): [BikeEvent] bikeEvents(offset: Int, limit: Int): [BikeEvent]
"If offset or limit is not provided, both values are ignored" "If offset or limit is not provided, both values are ignored"
@ -69,7 +71,7 @@ export default gql`
provider: Provider provider: Provider
"all participants currently engaged with the cargoBike" "all participants currently engaged with the cargoBike"
participants: [Participant] participants: [Participant]
insuranceData: InsuranceData! insuranceData: InsuranceData
lendingStation: LendingStation lendingStation: LendingStation
taxes: Taxes taxes: Taxes
currentEngagements: [Engagement] currentEngagements: [Engagement]
@ -83,6 +85,15 @@ export default gql`
lockedUntil: Date 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 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" "see column A in info tabelle"
group: Group! group: Group!
name: String! name: String!
state: BikeState
modelName: String! modelName: String!
numberOfWheels: Int! numberOfWheels: Int!
forCargo: Boolean! forCargo: Boolean!
@ -102,11 +114,11 @@ export default gql`
""" """
Does not refer to an extra table in the database. Does not refer to an extra table in the database.
""" """
technicalEquipment: TechnicalEquipmentCreateInput! technicalEquipment: TechnicalEquipmentCreateInput
""" """
Does not refer to an extra table in the database. 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 Refers to equipment that is not unique. See kommentierte info tabelle -> Fragen -> Frage 2
When set to null or [], no relations will be added. When set to null or [], no relations will be added.
@ -122,8 +134,8 @@ export default gql`
stickerBikeNameState: StickerBikeNameState stickerBikeNameState: StickerBikeNameState
note: String note: String
providerId: ID providerId: ID
insuranceData: InsuranceDataCreateInput! insuranceData: InsuranceDataCreateInput
taxes: TaxesCreateInput! taxes: TaxesCreateInput
} }
""" """
@ -134,6 +146,7 @@ export default gql`
"see column A in info tabelle" "see column A in info tabelle"
group: Group group: Group
name: String name: String
state: BikeState
modelName: String modelName: String
numberOfWheels: Int numberOfWheels: Int
forCargo: Boolean 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. Eventually, this field will become an enum or a separate data table and user can choose from a pool of insurance companies.
""" """
name: String! name: String
benefactor: String! benefactor: String
billing: String! billing: String
noPnP: String! noPnP: String
"eg. Anbieter, flotte, eigenleistung" "eg. Anbieter, flotte, eigenleistung"
maintenanceResponsible: String! maintenanceResponsible: String
maintenanceBenefactor: String! maintenanceBenefactor: String
maintenanceAgreement: String maintenanceAgreement: String
hasFixedRate: Boolean! hasFixedRate: Boolean
fixedRate: Float fixedRate: Float
""" """
Projektzuschuss: 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. Eventually, this field will become an enum or a separate data table and user can choose from a pool of insurance companies.
""" """
name: String! name: String
benefactor: String! benefactor: String
billing: String! billing: String
noPnP: String! noPnP: String
"eg. Anbieter, flotte, eigenleistung" "eg. Anbieter, flotte, eigenleistung"
maintenanceResponsible: String! maintenanceResponsible: String
maintenanceBenefactor: String! maintenanceBenefactor: String
maintenanceAgreement: String maintenanceAgreement: String
hasFixedRate: Boolean! hasFixedRate: Boolean
fixedRate: Float fixedRate: Float
""" """
Projektzuschuss: Projektzuschuss:
@ -254,17 +267,28 @@ export default gql`
notes: String 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." "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 { type DimensionsAndLoad {
hasCoverBox: Boolean! hasCoverBox: Boolean
"cover box can be locked" "cover box can be locked"
lockable: Boolean! lockable: Boolean
minBoxLength: Float boxLengthRange: NumRange
maxBoxLength: Float boxWidthRange: NumRange
minBoxWidth: Float boxHeightRange: NumRange
maxBoxWidth: Float
minBoxHeight: Float
maxBoxHeight: Float
maxWeightBox: Float maxWeightBox: Float
maxWeightLuggageRack: Float maxWeightLuggageRack: Float
maxWeightTotal: Float maxWeightTotal: Float
@ -275,14 +299,11 @@ export default gql`
} }
input DimensionsAndLoadCreateInput { input DimensionsAndLoadCreateInput {
hasCoverBox: Boolean! hasCoverBox: Boolean
lockable: Boolean! lockable: Boolean
minBoxLength: Float boxLengthRange: NumRangeInput
maxBoxLength: Float boxWidthRange: NumRangeInput
minBoxWidth: Float boxHeightRange: NumRangeInput
maxBoxWidth: Float
minBoxHeight: Float
maxBoxHeight: Float
maxWeightBox: Float maxWeightBox: Float
maxWeightLuggageRack: Float maxWeightLuggageRack: Float
maxWeightTotal: Float maxWeightTotal: Float
@ -295,12 +316,9 @@ export default gql`
input DimensionsAndLoadUpdateInput { input DimensionsAndLoadUpdateInput {
hasCoverBox: Boolean hasCoverBox: Boolean
lockable: Boolean lockable: Boolean
minBoxLength: Float boxLengthRange: NumRangeInput
maxBoxLength: Float boxWidthRange: NumRangeInput
minBoxWidth: Float boxHeightRange: NumRangeInput
maxBoxWidth: Float
minBoxHeight: Float
maxBoxHeight: Float
maxWeightBox: Float maxWeightBox: Float
maxWeightLuggageRack: Float maxWeightLuggageRack: Float
maxWeightTotal: Float maxWeightTotal: Float
@ -316,16 +334,16 @@ export default gql`
So no id needed for mutation. One Mutation for the CargoBike will be enough. So no id needed for mutation. One Mutation for the CargoBike will be enough.
""" """
type TechnicalEquipment { type TechnicalEquipment {
bicycleShift: String! bicycleShift: String
isEBike: Boolean! isEBike: Boolean
hasLightSystem: Boolean! hasLightSystem: Boolean
specialFeatures: String specialFeatures: String
} }
input TechnicalEquipmentCreateInput { input TechnicalEquipmentCreateInput {
bicycleShift: String! bicycleShift: String
isEBike: Boolean! isEBike: Boolean
hasLightSystem: Boolean! hasLightSystem: Boolean
specialFeatures: String specialFeatures: String
} }
@ -563,12 +581,12 @@ export default gql`
} }
type Taxes { type Taxes {
costCenter: String! costCenter: String
organisationArea: OrganisationArea organisationArea: OrganisationArea
} }
input TaxesCreateInput { input TaxesCreateInput {
costCenter: String! costCenter: String
organisationArea: OrganisationArea organisationArea: OrganisationArea
} }
@ -643,7 +661,7 @@ export default gql`
keepLock: Boolean 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 { type BikeEvent {
id: ID! id: ID!
bikeEventType: BikeEventType! bikeEventType: BikeEventType!
@ -923,13 +941,26 @@ export default gql`
loanTimes: [String!] loanTimes: [String!]
} }
"(dt. Zeitscheibe) When was a bike where" type DateRange{
type TimeFrame {
id: ID!
"format YYYY-MM-dd"
from: Date! from: Date!
"will be infinity of not omitted"
to: Date
}
input DateRangeInput {
"format YYYY-MM-dd" "format YYYY-MM-dd"
from: Date!
"""
format YYYY-MM-dd
will be infinity of not omitted
"""
to: Date to: Date
}
"(dt. Zeitscheibe) When was a bike where"
type TimeFrame {
id: ID!
dateRange: DateRange!
note: String note: String
lendingStation: LendingStation! lendingStation: LendingStation!
cargoBike: CargoBike! cargoBike: CargoBike!
@ -941,8 +972,7 @@ export default gql`
} }
input TimeFrameCreateInput { input TimeFrameCreateInput {
from: Date! dateRange: DateRangeInput!
to: Date
note: String note: String
lendingStationId: ID! lendingStationId: ID!
cargoBikeId: ID! cargoBikeId: ID!
@ -950,8 +980,7 @@ export default gql`
input TimeFrameUpdateInput { input TimeFrameUpdateInput {
id: ID! id: ID!
from: Date dateRange: DateRangeInput
to: Date
note: String note: String
lendingStationId: ID lendingStationId: ID
cargoBikeId: ID cargoBikeId: ID

Loading…
Cancel
Save