-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[mds-ingest-service] Add pagination and order support to getEvents via ingest #643
Changes from 3 commits
8235e29
ac998ae
3e0d597
f89ae4a
e63f378
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,11 +14,11 @@ | |
* limitations under the License. | ||
*/ | ||
|
||
import { Entity, Column, Index } from 'typeorm' | ||
import { BigintTransformer, IdentityColumn, RecordedColumn } from '@mds-core/mds-repository' | ||
import { Nullable, Timestamp } from '@mds-core/mds-types' | ||
import { Column, Entity, Index } from 'typeorm' | ||
import { EventDomainModel } from '../../@types' | ||
import { TelemetryEntity, TelemetryEntityModel } from './telemetry-entity' | ||
import { Nullable } from '@mds-core/mds-types' | ||
|
||
export interface EventEntityModel extends IdentityColumn, RecordedColumn { | ||
device_id: EventDomainModel['device_id'] | ||
|
@@ -42,7 +42,7 @@ export class EventEntity extends IdentityColumn(RecordedColumn(class {})) implem | |
provider_id: EventEntityModel['provider_id'] | ||
|
||
@Column('bigint', { transformer: BigintTransformer, primary: true }) | ||
timestamp: EventEntityModel['timestamp'] | ||
timestamp: Timestamp | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Strange type behavior here with the
Which corresponds to the Interestingly, this is only an issue when limit is passed. If the type declaration here is left as Changing this to This doesn't have any issues afaict, all previous tests are still passing with this change. π€· There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep. Weβve seen this before. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We've recently stopped defining entity properties in terms of Entity or (especially) Domain model properties and use the base types instead. Some libraries that use metadata reflection have issues tracing the aliases to the base type. In recent repositories, you will see a pattern something like: export class SomeEntity {
@Column(...)
entity_id: UUID
@Column(...)
timestamp: Timestamp
}
/* Create an alias to keep consistent with other services */
export SomeEntityModel = SomeEntity |
||
|
||
@Column('varchar', { array: true, length: 31 }) | ||
event_types: EventEntityModel['event_types'] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,16 +15,17 @@ | |
*/ | ||
|
||
import { InsertReturning, ReadWriteRepository, RepositoryError } from '@mds-core/mds-repository' | ||
import { Any } from 'typeorm' | ||
import { isUUID } from '@mds-core/mds-utils' | ||
import { UUID } from '@mds-core/mds-types' | ||
import { isUUID, ValidationError } from '@mds-core/mds-utils' | ||
import { Any } from 'typeorm' | ||
import { buildPaginator, Cursor } from 'typeorm-cursor-pagination' | ||
import { | ||
EventDomainModel, | ||
EventDomainCreateModel, | ||
TelemetryDomainCreateModel, | ||
DeviceDomainCreateModel, | ||
DeviceDomainModel, | ||
EventDomainCreateModel, | ||
GetVehicleEventsFilterParams, | ||
DeviceDomainModel | ||
GetVehicleEventsResponse, | ||
TelemetryDomainCreateModel | ||
} from '../@types' | ||
import entities from './entities' | ||
import { DeviceEntity } from './entities/device-entity' | ||
|
@@ -40,6 +41,8 @@ import { | |
} from './mappers' | ||
import migrations from './migrations' | ||
|
||
type VehicleEventsQueryParams = GetVehicleEventsFilterParams & Cursor | ||
|
||
/** | ||
* Aborts execution if not running under a test environment. | ||
*/ | ||
|
@@ -53,6 +56,17 @@ class IngestReadWriteRepository extends ReadWriteRepository { | |
super('ingest', { entities, migrations }) | ||
} | ||
|
||
private buildCursor = (cursor: VehicleEventsQueryParams) => | ||
Buffer.from(JSON.stringify(cursor), 'utf-8').toString('base64') | ||
|
||
private parseCursor = (cursor: string): VehicleEventsQueryParams & { limit: number } => { | ||
try { | ||
return JSON.parse(Buffer.from(cursor, 'base64').toString('utf-8')) | ||
} catch (error) { | ||
throw new ValidationError('Invalid cursor', error) | ||
} | ||
} | ||
|
||
public createEvents = async (events: EventDomainCreateModel[]) => { | ||
const { connect } = this | ||
try { | ||
|
@@ -114,10 +128,10 @@ class IngestReadWriteRepository extends ReadWriteRepository { | |
} | ||
} | ||
|
||
public getEvents = async (params: GetVehicleEventsFilterParams): Promise<EventDomainModel[]> => { | ||
private getEvents = async (params: VehicleEventsQueryParams): Promise<GetVehicleEventsResponse> => { | ||
const { connect } = this | ||
const { | ||
time_range: { start, end }, | ||
time_range, | ||
// geography_ids, | ||
grouping_type = 'latest_per_vehicle', | ||
event_types, | ||
|
@@ -127,8 +141,14 @@ class IngestReadWriteRepository extends ReadWriteRepository { | |
device_ids, | ||
propulsion_types, | ||
provider_ids, | ||
limit | ||
limit, | ||
beforeCursor, | ||
afterCursor, | ||
order | ||
} = params | ||
|
||
const { start, end } = time_range | ||
|
||
try { | ||
const connection = await connect('ro') | ||
|
||
|
@@ -199,14 +219,61 @@ class IngestReadWriteRepository extends ReadWriteRepository { | |
query.andWhere('events.provider_id = ANY(:provider_ids)', { provider_ids }) | ||
} | ||
|
||
const entities = await query.limit(limit).getMany() | ||
// Use query instead of paginator to manage order if using a joined field | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. couldn't figure out how to pass an order column string for a joined field to buildPaginator without it throwing an error, if there's a better way to handle this field happy to change it. |
||
if (order && order.column === 'vehicle_id') { | ||
query.orderBy('devices.vehicle_id', order.direction) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm unsure how the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call out, I took a look at this and it turns out that There seems to be an open issue for this (can't sort on relational tables / override So for now, removed the ability to sort by That being said, sorting on joined tables will most likely come up in the future at some point - so hopefully this gets addressed in typeorm-cursor-pagination shortly. PS - updated tests since apparently they were false positives for sorting on vehicle_id. |
||
} | ||
|
||
const pager = buildPaginator({ | ||
entity: EventEntity, | ||
alias: 'events', | ||
paginationKeys: ['timestamp', 'id'], | ||
query: { | ||
limit, | ||
order: order?.direction ?? (order?.column === undefined ? 'DESC' : 'ASC'), | ||
beforeCursor: beforeCursor ?? undefined, // typeorm-cursor-pagination type weirdness | ||
afterCursor: afterCursor ?? undefined // typeorm-cursor-pagination type weirdness | ||
} | ||
}) | ||
|
||
return entities.map(EventEntityToDomain.map) | ||
const { | ||
data, | ||
cursor: { beforeCursor: nextBeforeCursor, afterCursor: nextAfterCursor } | ||
} = await pager.paginate(query) | ||
|
||
const cursor = { | ||
time_range, | ||
// geography_ids, | ||
grouping_type, | ||
event_types, | ||
vehicle_states, | ||
vehicle_types, | ||
vehicle_id, | ||
device_ids, | ||
propulsion_types, | ||
provider_ids, | ||
limit, | ||
order | ||
} | ||
|
||
return { | ||
events: data.map(EventEntityToDomain.map), | ||
cursor: { | ||
next: nextAfterCursor && this.buildCursor({ ...cursor, beforeCursor: null, afterCursor: nextAfterCursor }), | ||
prev: nextBeforeCursor && this.buildCursor({ ...cursor, beforeCursor: nextBeforeCursor, afterCursor: null }) | ||
} | ||
} | ||
} catch (error) { | ||
throw RepositoryError(error) | ||
} | ||
} | ||
|
||
public getEventsUsingOptions = async (options: GetVehicleEventsFilterParams): Promise<GetVehicleEventsResponse> => | ||
this.getEvents({ ...options, beforeCursor: null, afterCursor: null, limit: options.limit ?? 100 }) | ||
|
||
public getEventsUsingCursor = async (cursor: string): Promise<GetVehicleEventsResponse> => | ||
this.getEvents(this.parseCursor(cursor)) | ||
|
||
/** | ||
* Nukes everything from orbit. Boom. | ||
*/ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious as I've seen this in other places as I learn typescript. Is it common to name the value and type name the same? I've seen it where the value name is descriptive and would name it like "GetVehicleEventOrderColumns" (with an s to describe these are all the columns) and the type is then without the "s". Also, is it preferred to use vs "as const"? Are they the same?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've seen instances of both
and
In various places in the code base. I think the latter is the preferred.
My assumption is that the latter just reduces the overhead of knowing which type to choose (
FooTypes vs FooTypes
) since TS will interpret it as the type declaration. (@drtyh2o feel free to tell me I'm off base here π ).As for
<const> foo
vsfoo as const
, they are equivalent except in a.tsx
file.