import distance from '@turf/distance' import { point } from '@turf/helpers' import { castArray, sortBy } from 'lodash' import { ElementDataWrapper, ElementSchemaVersion } from '../interfaces' import { GPSCoordinates } from '../interfaces/gis.interface' import { IListingConfig, IListingElementsResponse, ListingOptions, ListingQuery, ParsedListingConfig, ParsedListingParams, QueryCriteria, RawCriteria, SerializableListingParams, } from '../interfaces/listing.interface' import { Element, GPS_COORDINATES, META_ATTRIBUTES } from '../models' import { getListingConfigFromMongoQuery, LISTING_PER_PAGE } from '../services' import type { ApiResponse } from '../services/api' import { isArrayLike } from '../util/isArrayLike' import { DynamicContext } from '../util/parseDynamicValue' import { transformType } from '../util/transformType' import { MainStore } from './mainStore' export class ListingStore { main: MainStore constructor(main: MainStore) { this.main = main } transformTypes(types: IListingConfig['type']): string[] | null { const organization = this.main.userStore.organization.id let arrayType: string[] | null if (types) { arrayType = castArray(types).map((t) => transformType(t, organization)) } else { arrayType = null } return arrayType } getDynamicContext(elementContext?: Element): DynamicContext { return { userElement: this.main.userStore.userElement, uid: this.main.userStore.user?.id, element: elementContext, } } parseCriteria( criteria: QueryCriteria, context?: DynamicContext, preventDefaultArchive?: boolean, ): RawCriteria { const organization = this.main.userStore.organization.id // Archived only by default, if archived is NULL then return all const archivedFilter: Record = criteria.archived === null || preventDefaultArchive === true ? {} : { 'archived.value': { $ne: 'true' }, } const parsedCriteria = this.main.listing.parseCriteria(criteria, context, { ...archivedFilter, organization, }) return parsedCriteria } parseConfig( config: IListingConfig | ParsedListingConfig, elementContext?: Element, options?: ListingOptions, ): ParsedListingParams { const organization = this.main.userStore.organization.id const forceVersion = this.main.userStore.schemaVersion const isParsed = 'parsed' in config && config.parsed === true const context: DynamicContext = this.getDynamicContext(elementContext) const transformedTypes = isParsed ? config.type : this.transformTypes(config.type) const parsedCriteria = isParsed ? (config.criteria as RawCriteria) : this.parseCriteria( config.criteria as QueryCriteria, context, config.preventDefaultArchive, ) const sort = Array.isArray(config.sort) ? config.sort[0] : config.sort const cleanedProjection = config.projection.filter( (p) => !META_ATTRIBUTES.includes(p), ) const params: SerializableListingParams = [ transformedTypes, parsedCriteria, cleanedProjection, sort, config.page, config.limit, config.atTime, { forceVersion }, options, config.preventDefaultArchive, ] const dataHash = this.main.listing.getListingRequestHash( params, organization, context, ) return { params, dataHash, parsedConfig: { type: transformedTypes, criteria: parsedCriteria, projection: cleanedProjection, sort: sort, page: config.page, limit: config.limit, atTime: config.atTime, preventDefaultArchive: config.preventDefaultArchive, parsed: true, // this is to prevent double-parsing }, tags: config.tags, } } async get( type: string | string[] | null, criteria: QueryCriteria, projection: string[], sort?: Record, page = 1, limit = LISTING_PER_PAGE, elementContext?: Element, atTime?: string, skipParsing = false, options?: ListingOptions, preventDefaultArchive?: boolean, ) { const { params, dataHash, parsedConfig } = this.parseConfig( { type, criteria, projection, sort, page, limit, atTime, preventDefaultArchive, parsed: skipParsing === true ? true : undefined, }, elementContext, options, ) if (parsedConfig.type) { if (this.main.userStore.schemaVersion === ElementSchemaVersion.V1) { await Promise.all( parsedConfig.type.map((t) => this.main.elementStore.getElementTypeData(t), ), ) } else if ( this.main.userStore.schemaVersion === ElementSchemaVersion.V2 ) { await Promise.all( parsedConfig.type .map((t) => [ this.main.elementStore.getElementSchema(t), this.main.elementStore.getTemplate(t), ]) .flat(), ) } } if ( !this.main.connectivityService || this.main.connectivityService.isConnected ) { const result = await this.main.listing.get(...params) if (result.data) { await this.main.offlineService?.saveListing(dataHash, result.data) } if (elementContext) { elementContext.addInteralListingHash(dataHash) } return result } else { const cached = await this.main.offlineService?.getListing(dataHash) if (elementContext) { elementContext.addInteralListingHash(dataHash) } return { ok: true, problem: null, originalError: null, data: cached, } as ApiResponse } } // Za tento TS definition som na seba velmi hrdy /epick async getElements( ...args: Parameters ): Promise | null> { const listingRequest = await this.get(...args) if (!listingRequest.ok) { throw listingRequest.originalError } const elementsData: ElementDataWrapper[] = listingRequest.data || [] if (!isArrayLike(elementsData)) { // console.warn('listing results are not array'); return null } return { elements: elementsData .map((e) => { if (!e || !e.element) { return } return this.main.elementStore.instantiateElement( e.element.hash, e.element, ) as T }) .filter(Boolean) as T[], count: Number(listingRequest.headers?.['x-count']) || elementsData.length, asyncListingInitiated: listingRequest.headers?.['x-async-listing'] === 'true', } } async getFirstElement( ...args: Parameters ): Promise { // set query limit to 10 as we only need the first result args[5] = 1 const listingResult = await this.getElements(...args) if (!listingResult || listingResult.elements.length === 0) { return null } return listingResult.elements[0] } async loadListingConfig( config: IListingConfig | ParsedListingConfig, elementContext?: Element, currentPosition?: GPSCoordinates, ): Promise> { // automatically add `coordinates` if sorting by distance const enhancedProjection = config.localSortByDistance || config.localSortByElementDistance ? [...new Set([...config.projection, GPS_COORDINATES])] : config.projection const sort = Array.isArray(config.sort) ? config.sort[0] : config.sort const result = await this.getElements( config.type, config.criteria as QueryCriteria, enhancedProjection, sort, config.page, config.limit, elementContext, config.atTime, 'parsed' in config ? config.parsed : false, undefined, config.preventDefaultArchive, ) const count = result?.count || 0 const asyncListingInitiated = result?.asyncListingInitiated // there can be a custom ordering not supported by backend let localSorted: Element[] if (config.localSort) { localSorted = await config.localSort(result?.elements || []) } else { localSorted = result?.elements || [] } // sorting by distance if (config.localSortByDistance && currentPosition) { const elements = this.sortByDistanceToPoint(localSorted, currentPosition) return { elements, count, asyncListingInitiated } } else if (config.localSortByElementDistance) { const elementPosition = config.localSortByElementDistance.getAttributeValue( GPS_COORDINATES, ) as [number, number] if (!elementPosition) { throw new Error('Reference element has no coordinates') } const elements = this.sortByDistanceToPoint(localSorted, { longitude: elementPosition[0], latitude: elementPosition[1], }) return { elements, count, asyncListingInitiated } } else { const elements = localSorted return { elements, count, asyncListingInitiated } } } async loadElementQuery(query: ListingQuery, elementContext?: Element) { const { type, criteria, projection } = getListingConfigFromMongoQuery(query) return this.getElements( type, criteria, projection, query.sort || { element_name: 1, // sort by element_name default as on FE }, query.paginate?.page || 1, query.paginate?.['per-page'] || 100, elementContext, ) } private sortByDistanceToPoint = ( elements: Element[], reference: GPSCoordinates, ) => { return sortBy(elements, (element) => { const elementPosition = element.getAttributeValue(GPS_COORDINATES) as [ number, number, ] if (!elementPosition) { return Number.MAX_SAFE_INTEGER } const elementPoint = point(elementPosition) const positionPoint = point([reference.longitude, reference.latitude]) const pointDistance = distance(elementPoint, positionPoint) return pointDistance }) } }