Skip to main content

Overview

The PointsService is a NestJS service class that provides business logic for working with Point entities. It uses the TypeORM repository pattern to interact with the partitioned points table.

Service Declaration

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Point } from './model/point.model';

@Injectable()
export class PointsService {
    constructor(
        @InjectRepository(Point)
        private readonly pointsRepository: Repository<Point>,
    ) { }
}

Dependency Injection

The service uses TypeORM’s @InjectRepository decorator to inject the Point repository.
pointsRepository
Repository<Point>
TypeORM repository for executing queries and operations on the points table.

Repository Pattern

The pointsRepository provides access to TypeORM’s full query API:

Available Repository Methods

find()
Promise<Point[]>
Find multiple points matching criteria.
const points = await this.pointsRepository.find({
  where: { trackerDeviceImei: deviceImei },
  order: { timestamp: 'DESC' },
  take: 100
});
findOne()
Promise<Point | null>
Find a single point by criteria.
const point = await this.pointsRepository.findOne({
  where: { id: pointId, timestamp: timestamp }
});
save()
Promise<Point>
Insert or update a point.
const newPoint = this.pointsRepository.create({
  trackerDeviceImei: 123456789012345,
  timestamp: Date.now(),
  trackerDeviceLatitude: 19.4326,
  trackerDeviceLongitude: -99.1332,
  // ... other fields
});

await this.pointsRepository.save(newPoint);
createQueryBuilder()
SelectQueryBuilder<Point>
Create complex queries with joins, aggregations, and raw SQL.
const avgSpeed = await this.pointsRepository
  .createQueryBuilder('point')
  .select('AVG(point.tracker_device_speed_kh)', 'avg_speed')
  .where('point.tracker_device_imei = :imei', { imei })
  .andWhere('point.timestamp >= :start', { start: startTime })
  .getRawOne();

Common Service Methods (Extensible)

While the current implementation is minimal, here are typical methods you would add to PointsService:

Create Point

async create(pointData: Partial<Point>): Promise<Point> {
  const point = this.pointsRepository.create({
    ...pointData,
    timestamp: pointData.timestamp || Date.now()
  });
  
  return await this.pointsRepository.save(point);
}

Bulk Insert

async createBulk(points: Partial<Point>[]): Promise<Point[]> {
  const entities = this.pointsRepository.create(points);
  return await this.pointsRepository.save(entities, { chunk: 1000 });
}

Find by Time Range

import { Between } from 'typeorm';

async findByTimeRange(
  deviceImei: number,
  startTime: number,
  endTime: number
): Promise<Point[]> {
  return await this.pointsRepository.find({
    where: {
      trackerDeviceImei: deviceImei,
      timestamp: Between(startTime, endTime)
    },
    order: { timestamp: 'ASC' }
  });
}

Find Latest Point

async findLatest(deviceImei: number): Promise<Point | null> {
  return await this.pointsRepository.findOne({
    where: { trackerDeviceImei: deviceImei },
    order: { timestamp: 'DESC' }
  });
}

Spatial Query (Within Radius)

async findNearLocation(
  latitude: number,
  longitude: number,
  radiusMeters: number,
  startTime: number
): Promise<Point[]> {
  return await this.pointsRepository
    .createQueryBuilder('point')
    .where('point.timestamp >= :startTime', { startTime })
    .andWhere(
      `ST_DWithin(
        point.location::geography,
        ST_MakePoint(:lon, :lat)::geography,
        :radius
      )`,
      { lat: latitude, lon: longitude, radius: radiusMeters }
    )
    .orderBy('point.timestamp', 'DESC')
    .getMany();
}

Calculate Daily Distance

async getDailyDistance(
  deviceImei: number,
  date: Date
): Promise<number> {
  const startOfDay = new Date(date).setHours(0, 0, 0, 0);
  const endOfDay = new Date(date).setHours(23, 59, 59, 999);
  
  const result = await this.pointsRepository
    .createQueryBuilder('point')
    .select('MAX(point.tracker_device_total_distance) - MIN(point.tracker_device_total_distance)', 'distance')
    .where('point.tracker_device_imei = :imei', { imei: deviceImei })
    .andWhere('point.timestamp BETWEEN :start AND :end', {
      start: startOfDay,
      end: endOfDay
    })
    .getRawOne();
  
  return result?.distance || 0;
}

Get Vehicle Route

async getRoute(
  deviceImei: number,
  startTime: number,
  endTime: number,
  minDistanceBetweenPoints: number = 100 // meters
): Promise<Point[]> {
  // Returns points that are at least minDistanceBetweenPoints apart
  // to reduce noise and create a cleaner route visualization
  
  const query = `
    WITH numbered_points AS (
      SELECT *,
        LAG(location) OVER (ORDER BY timestamp) AS prev_location
      FROM points
      WHERE tracker_device_imei = $1
        AND timestamp BETWEEN $2 AND $3
    )
    SELECT *
    FROM numbered_points
    WHERE prev_location IS NULL
       OR ST_Distance(location::geography, prev_location::geography) >= $4
    ORDER BY timestamp ASC
  `;
  
  return await this.pointsRepository.query(query, [
    deviceImei,
    startTime,
    endTime,
    minDistanceBetweenPoints
  ]);
}

Injecting the Service

To use PointsService in other parts of your application:
import { Injectable } from '@nestjs/common';
import { PointsService } from './points.service';

@Injectable()
export class VehicleTrackingService {
  constructor(private readonly pointsService: PointsService) {}
  
  async getVehicleCurrentLocation(imei: number) {
    const latestPoint = await this.pointsService.findLatest(imei);
    
    return {
      latitude: latestPoint.trackerDeviceLatitude,
      longitude: latestPoint.trackerDeviceLongitude,
      timestamp: latestPoint.timestamp,
      speed: latestPoint.trackerDeviceSpeedKh,
      heading: latestPoint.trackerDeviceAngle
    };
  }
}

Module Registration

Ensure PointsService is registered in your module:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Point } from './model/point.model';
import { PointsService } from './points.service';
import { PointsController } from './points.controller';

@Module({
  imports: [TypeOrmModule.forFeature([Point])],
  controllers: [PointsController],
  providers: [PointsService],
  exports: [PointsService] // Export if used by other modules
})
export class PointsModule {}

Performance Considerations

Partitioning Awareness

Always include timestamp in your WHERE clause to enable partition pruning. Queries without time constraints will scan all partitions.
// Good - partition pruning enabled
await this.pointsRepository.find({
  where: {
    trackerDeviceImei: imei,
    timestamp: Between(yesterday, now)
  }
});

// Bad - full table scan across all partitions
await this.pointsRepository.find({
  where: { trackerDeviceImei: imei }
});

Indexing Strategy

Create indexes to optimize common queries:
-- Index for device + time range queries
CREATE INDEX idx_points_imei_timestamp 
ON points (tracker_device_imei, timestamp DESC);

-- Spatial index for location queries
CREATE INDEX idx_points_location 
ON points USING GIST (location);

-- Index for trip analysis
CREATE INDEX idx_points_trip 
ON points (tracker_device_trip_id, timestamp);

Batch Operations

For bulk inserts, use chunking to avoid memory issues:
async bulkInsertPoints(points: Partial<Point>[]): Promise<void> {
  const chunkSize = 1000;
  
  for (let i = 0; i < points.length; i += chunkSize) {
    const chunk = points.slice(i, i + chunkSize);
    const entities = this.pointsRepository.create(chunk);
    await this.pointsRepository.save(entities, { chunk: 500 });
  }
}

Next Steps

Data Model

Complete Point entity field reference

API Overview

Points API concepts and patterns

Build docs developers (and LLMs) love