import { providers } from '@mds-core/mds-providers' ; import { Geography , Policy , VehicleEvent , VEHICLE_TYPES , DAYS_OF_WEEK , AUDIT_EVENT_TYPES , VEHICLE_EVENTS , AUDIT_EVENT_TYPE , VEHICLE_EVENT , RULE_TYPES , UUID , Timestamp , Telemetry , Stop , PROPULSION_TYPES , VEHICLE_STATUSES , Device } from '@mds-core/mds-types' ; import * as Joi from '@hapi/joi' ; import joiToJsonSchema from 'joi-to-json-schema' ; import { ValidationError } from '@mds-core/mds-utils' ; export { ValidationError } ; interface ValidatorOptions { property : string ; assert : boolean ; required : boolean ; allowUnknown : boolean ; } export const stringSchema = Joi . string ( ) . empty ( '' ) ; export const numberSchema = Joi . number ( ) . options ( { convert : false } ) ; export const uuidSchema = stringSchema . guid ( ) ; export const timestampSchema = numberSchema . min ( 1420099200000 ) ; export const providerIdSchema = uuidSchema . valid ( Object . keys ( providers ) ) ; const vehicleIdSchema = stringSchema . max ( 255 ) ; const telemetrySchema = Joi . object ( ) . keys ( { gps : Joi . object ( ) . keys ( { lat : numberSchema . min ( - 90 ) . max ( 90 ) . required ( ) , lng : numberSchema . min ( - 180 ) . max ( 180 ) . required ( ) , speed : numberSchema . optional ( ) , heading : numberSchema . optional ( ) , accuracy : numberSchema . optional ( ) , hdop : numberSchema . optional ( ) , altitude : numberSchema . optional ( ) , satellites : numberSchema . optional ( ) } ) . required ( ) , charge : numberSchema . optional ( ) , provider_id : providerIdSchema . optional ( ) , device_id : uuidSchema . optional ( ) , timestamp : timestampSchema . required ( ) , recorded : timestampSchema . optional ( ) } ) ; const ruleSchema = Joi . object ( ) . keys ( { name : Joi . string ( ) . required ( ) , rule_id : Joi . string ( ) . guid ( ) . required ( ) , rule_type : Joi . string ( ) . valid ( Object . values ( RULE_TYPES ) ) . required ( ) , rule_units : Joi . string ( ) . valid ( [ 'seconds' , 'minutes' , 'hours' , 'mph' , 'kph' ] ) , geographies : Joi . array ( ) . items ( Joi . string ( ) . guid ( ) ) , statuses : Joi . object ( ) . keys ( { available : Joi . array ( ) , reserved : Joi . array ( ) , unavailable : Joi . array ( ) , removed : Joi . array ( ) , inactive : Joi . array ( ) , trip : Joi . array ( ) , elsewhere : Joi . array ( ) } ) . allow ( null ) , vehicle_types : Joi . array ( ) . items ( Joi . string ( ) . valid ( Object . values ( VEHICLE_TYPES ) ) ) , maximum : Joi . number ( ) , minimum : Joi . number ( ) , start_time : Joi . string ( ) , end_time : Joi . string ( ) , days : Joi . array ( ) . items ( Joi . string ( ) . valid ( Object . values ( DAYS_OF_WEEK ) ) ) , messages : Joi . object ( ) , value_url : Joi . string ( ) . uri ( ) } ) ; export const policySchema = Joi . object ( ) . keys ( { name : Joi . string ( ) . required ( ) , description : Joi . string ( ) . required ( ) , policy_id : Joi . string ( ) . guid ( ) . required ( ) , start_date : Joi . date ( ) . timestamp ( 'javascript' ) . required ( ) , end_date : Joi . date ( ) . timestamp ( 'javascript' ) . allow ( null ) , prev_policies : Joi . array ( ) . items ( Joi . string ( ) . guid ( ) ) . allow ( null ) , provider_ids : Joi . array ( ) . items ( Joi . string ( ) . guid ( ) ) . allow ( null ) , rules : Joi . array ( ) . min ( 1 ) . items ( ruleSchema ) . required ( ) } ) ; const policiesSchema = Joi . array ( ) . items ( policySchema ) ; const featureSchema = Joi . object ( ) . keys ( { type : Joi . string ( ) . valid ( [ 'Feature' ] ) . required ( ) , properties : Joi . object ( ) . required ( ) , geometry : Joi . object ( ) . required ( ) } ) . unknown ( true ) ; const featureCollectionSchema = Joi . object ( ) . keys ( { type : Joi . string ( ) . valid ( [ 'FeatureCollection' ] ) . required ( ) , features : Joi . array ( ) . min ( 1 ) . items ( featureSchema ) . required ( ) } ) . unknown ( true ) ; export const geographySchema = Joi . object ( ) . keys ( { geography_id : Joi . string ( ) . guid ( ) . required ( ) , geography_json : featureCollectionSchema , previous_geography_ids : Joi . array ( ) . items ( Joi . string ( ) . guid ( ) ) . allow ( null ) , name : Joi . string ( ) . required ( ) } ) ; const geographiesSchema = Joi . array ( ) . items ( geographySchema ) ; const eventsSchema = Joi . array ( ) . items ( ) ; const vehicleEventTypeSchema = stringSchema . valid ( Object . keys ( VEHICLE_EVENTS ) ) ; const vehicleTypeSchema = stringSchema . valid ( Object . keys ( VEHICLE_TYPES ) ) ; const propulsionTypeSchema = stringSchema . valid ( Object . keys ( PROPULSION_TYPES ) ) ; const vehicleStatusSchema = stringSchema . valid ( Object . keys ( VEHICLE_STATUSES ) ) ; const eventSchema = Joi . object ( ) . keys ( { device_id : uuidSchema . required ( ) , provider_id : uuidSchema . required ( ) , timestamp : timestampSchema . required ( ) , event_type : vehicleEventTypeSchema . required ( ) , telemetry_timestamp : timestampSchema . optional ( ) , telemetry : telemetrySchema . required ( ) , service_area_id : uuidSchema . allow ( null ) . optional ( ) , recorded : timestampSchema . optional ( ) } ) ; const tripEventSchema = eventSchema . keys ( { trip_id : uuidSchema . required ( ) } ) ; const serviceEndEventSchema = eventSchema . keys ( { event_type_reason : stringSchema . valid ( [ 'low_battery' , 'maintenance' , 'compliance' , 'off_hours' ] ) . required ( ) } ) ; const providerPickUpEventSchema = eventSchema . keys ( { event_type_reason : stringSchema . valid ( [ 'rebalance' , 'maintenance' , 'charge' , 'compliance' ] ) . required ( ) } ) ; const deregisterEventSchema = eventSchema . keys ( { event_type_reason : stringSchema . valid ( [ 'missing' , 'decomissioned' ] ) . required ( ) } ) ; const auditEventTypeSchema = ( accept ? ) : Joi . StringSchema => stringSchema . valid ( accept || Object . keys ( AUDIT_EVENT_TYPES ) ) ; const auditIssueCodeSchema = stringSchema . max ( 31 ) ; const auditNoteSchema = stringSchema . max ( 255 ) ; const vehicleTypesCountMapSchema = Joi . object ( ) . keys ( { scooter : Joi . number ( ) , bicycle : Joi . number ( ) , car : Joi . number ( ) , moped : Joi . number ( ) } ) ; const stopSchema = Joi . object ( ) . keys ( { stop_id : uuidSchema . required ( ) , stop_name : stringSchema . required ( ) , short_name : stringSchema . optional ( ) , platform_code : stringSchema . optional ( ) , geography_id : uuidSchema . optional ( ) , lat : numberSchema . min ( - 90 ) . max ( 90 ) . required ( ) , lng : numberSchema . min ( - 180 ) . max ( 180 ) . required ( ) , zone_id : uuidSchema . optional ( ) , address : stringSchema . optional ( ) , post_code : stringSchema . optional ( ) , rental_methods : stringSchema . optional ( ) , capacity : vehicleTypesCountMapSchema . required ( ) , location_type : stringSchema . optional ( ) , timezone : stringSchema . optional ( ) , cross_street : stringSchema . optional ( ) , num_vehicles_available : vehicleTypesCountMapSchema . required ( ) , num_vehicles_disabled : vehicleTypesCountMapSchema . optional ( ) , num_spots_available : vehicleTypesCountMapSchema . required ( ) , num_spots_disabled : vehicleTypesCountMapSchema . optional ( ) , wheelchair_boarding : Joi . bool ( ) , reservation_cost : vehicleTypesCountMapSchema . optional ( ) } ) ; const deviceSchema = Joi . object ( ) . keys ( { device_id : uuidSchema . required ( ) , provider_id : uuidSchema . required ( ) , vehicle_id : stringSchema . required ( ) , type : vehicleTypeSchema . required ( ) , propulsion : Joi . array ( ) . items ( propulsionTypeSchema ) . required ( ) , year : numberSchema . optional ( ) , mfgr : stringSchema . optional ( ) , model : stringSchema . optional ( ) , recorded : timestampSchema . optional ( ) , status : vehicleStatusSchema } ) ; const Format = ( property , error ) : string => { const [ { message , path } ] = error . details ; const [ , ... details ] = message . split ( ' ' ) ; return `${ [ property , ... path ] . join ( '.' ) } ${ details . join ( ' ' ) }` ; } ; export const ValidateSchema = < T = unknown > ( value , schema , options = { } ) : value is T => { const { assert = true , required = true , property = 'value' , allowUnknown = false } = options ; const { error } = Joi . validate ( value , schema , { presence : required ? 'required' : 'optional' , allowUnknown } ) ; if ( error && assert ) { throw new ValidationError ( `invalid_${ property }` . toLowerCase ( ) , { [ property ] : value , details : Format ( property , error ) } ) ; } return ! error ; } ; interface NumberValidatorOptions extends ValidatorOptions { min : number ; max : number ; } export const isValidNumber = ( value , options = { } ) : value is number => ValidateSchema ( value , numberSchema . min ( options . min === undefined ? Number . MIN_SAFE_INTEGER : options . min ) . max ( options . max === undefined ? Number . MAX_SAFE_INTEGER : options . max ) , options ) ; export const isValidAuditTripId = ( audit_trip_id , options = { } ) : audit_trip_id is UUID => ValidateSchema ( audit_trip_id , uuidSchema , { property : 'audit_trip_id' , ... options } ) ; interface AuditEventValidatorOptions extends ValidatorOptions { accept : AUDIT_EVENT_TYPE [ ] ; } export const isValidStop = ( value ) : value is Stop => { const { error } = Joi . validate ( value , stopSchema ) ; if ( error ) { throw new ValidationError ( 'invalid_stop' , { value , details : Format ( 'stop' , error ) } ) ; } return true ; } ; export const isValidDeviceId = ( value , options = { } ) : value is UUID => ValidateSchema ( value , uuidSchema , { property : 'device_id' , ... options } ) ; export const isValidAuditEventType = ( value , { accept , ... options } = { } ) : value is AUDIT_EVENT_TYPE => ValidateSchema ( value , auditEventTypeSchema ( accept ) , { property : 'audit_event_type' , ... options } ) ; export const isValidTimestamp = ( value , options = { } ) : value is Timestamp => ValidateSchema ( value , timestampSchema , { property : 'timestamp' , ... options } ) ; export const isValidProviderId = ( value , options = { } ) : value is UUID => ValidateSchema ( value , providerIdSchema , { property : 'provider_id' , ... options } ) ; export const isValidProviderVehicleId = ( value , options = { } ) : value is string => ValidateSchema ( value , vehicleIdSchema , { property : 'provider_vehicle_id' , ... options } ) ; export const isValidAuditEventId = ( value , options = { } ) : value is UUID => ValidateSchema ( value , uuidSchema , { property : 'audit_event_id' , ... options } ) ; export const isValidAuditDeviceId = ( value , options = { } ) : value is UUID => ValidateSchema ( value , uuidSchema , { property : 'audit_device_id' , ... options } ) ; export const isValidTelemetry = ( value , options = { } ) : value is Telemetry => ValidateSchema ( value , telemetrySchema , { property : 'telemetry' , ... options } ) ; export const isValidDevice = ( value , options = { } ) : value is Device => ValidateSchema ( value , deviceSchema , options ) ; export const isValidEvent = ( value , options = { } ) : value is VehicleEvent => ValidateSchema ( value , eventSchema , options ) ; export const isValidVehicleEventType = ( value , options = { } ) : value is VEHICLE_EVENT => ValidateSchema ( value , vehicleEventTypeSchema , { property : 'vehicle_event_type' , ... options } ) ; export const isValidAuditIssueCode = ( value , options = { } ) : value is string => ValidateSchema ( value , auditIssueCodeSchema , { property : 'audit_issue_code' , ... options } ) ; export const isValidAuditNote = ( value , options = { } ) : value is string => ValidateSchema ( value , auditNoteSchema , { property : 'note' , ... options } ) ; export const HasPropertyAssertion = < T > ( obj , ... props ) : obj is T => typeof obj === 'object' && obj !== null && props . every ( prop => prop in obj ) ; export function validatePolicies ( policies ) { const { error } = Joi . validate ( policies , policiesSchema ) ; if ( error ) { throw new ValidationError ( 'invalid_policies' , { policies , details : Format ( 'policies' , error ) } ) ; } return true ; } export function validateGeographies ( geographies ) { const { error } = Joi . validate ( geographies , geographiesSchema ) ; if ( error ) { throw new ValidationError ( 'invalid_geographies' , { geographies , details : Format ( 'geographies' , error ) } ) ; } return true ; } export function validateEvents ( events ) { const { error } = Joi . validate ( events , eventsSchema ) ; if ( error ) { throw new ValidationError ( 'invalid events' , { events , details : Format ( 'events' , error ) } ) ; } return true ; } export function policyValidationDetails ( policy ) { const { error } = Joi . validate ( policy , policySchema , { allowUnknown : false } ) ; if ( error ) { return error . details ; } return null ; } export function geographyValidationDetails ( geography ) { const { error } = Joi . validate ( geography , geographySchema , { allowUnknown : false } ) ; if ( error ) { return error . details ; } return null ; } export function rawValidatePolicy ( policy ) { return Joi . validate ( policy , policySchema ) ; } const validateTripEvent = ( event ) => ValidateSchema ( event , tripEventSchema , { } ) ; const validateProviderPickUpEvent = ( event ) => ValidateSchema ( event , providerPickUpEventSchema , { } ) ; const validateServiceEndEvent = ( event ) => ValidateSchema ( event , serviceEndEventSchema , { } ) ; const validateDeregisterEvent = ( event ) => ValidateSchema ( event , deregisterEventSchema , { } ) ; export const validateEvent = ( event ) => { if ( isValidEvent ( event , { allowUnknown : true } ) ) { const { event_type } = event ; const TRIP_EVENTS = [ VEHICLE_EVENTS . trip_start , VEHICLE_EVENTS . trip_end , VEHICLE_EVENTS . trip_enter , VEHICLE_EVENTS . trip_leave ] ; if ( TRIP_EVENTS . includes ( event_type ) ) { return validateTripEvent ( event ) ; } if ( event_type === VEHICLE_EVENTS . provider_pick_up ) { return validateProviderPickUpEvent ( event ) ; } if ( event_type === VEHICLE_EVENTS . service_end ) { return validateServiceEndEvent ( event ) ; } if ( event_type === VEHICLE_EVENTS . deregister ) { return validateDeregisterEvent ( event ) ; } return ValidateSchema ( event , eventSchema , { } ) ; } } ; export const policySchemaJson = joiToJsonSchema ( policySchema ) ; export const SchemaBuilder = Joi ;