diff --git a/src/coercion/parseDate.test.ts b/src/coercion/parseDate.test.ts new file mode 100644 index 0000000..7e455bf --- /dev/null +++ b/src/coercion/parseDate.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "@jest/globals"; +import { InvalidArgumentError } from "commander"; +import parseDate from "./parseDate"; + +describe("Date argument parser", () => { + it("should coerse ISO 8601 strings to objects", () => { + const isoDateFrom = "2023-06-29T12:34:56Z"; + const expected = new Date(Date.UTC(2023, 5, 29, 12, 34, 56, 0)); + + const parsedDate = parseDate(isoDateFrom) + + expect(parsedDate).toEqual(expected); + }); + + it("should raise an error for invalid values", () => { + const malformedDateFrom = "2023-06-29T123456Z"; + expect(() => parseDate(malformedDateFrom)).toThrow(InvalidArgumentError); + }); +}); \ No newline at end of file diff --git a/src/coercion/parseDate.ts b/src/coercion/parseDate.ts new file mode 100644 index 0000000..6475089 --- /dev/null +++ b/src/coercion/parseDate.ts @@ -0,0 +1,16 @@ +import { InvalidArgumentError } from "commander"; + +/** + * @param dateString an ISO 8601 formatted datetime argument + * @returns The argument coerced into a `Date` object + * @throws {InvalidArgumentError} must be a valid ISO 8601 formatted `string` + */ +export default (dateString: string): Date => { + const parsedDate = new Date(dateString); + + if (isNaN(parsedDate.getTime())) { + throw new InvalidArgumentError('Invalid datetime format'); + } + + return parsedDate; + }; \ No newline at end of file diff --git a/src/commands/get.ts b/src/commands/get.ts index 25b2cae..e38eed9 100644 --- a/src/commands/get.ts +++ b/src/commands/get.ts @@ -12,7 +12,7 @@ import { } from "../privacy/filter"; import { loadSession } from "../login-session"; -export const get = async (moduleName: string, privacyPass?: string) => { +export const get = async (moduleName: string, dateFrom?: Date, privacyPass?: string) => { const module = getModuleByName(moduleName); if (module == null) { @@ -48,7 +48,7 @@ export const get = async (moduleName: string, privacyPass?: string) => { ); } - const ocpiObjectStream = fetchDataForModule(session, module); + const ocpiObjectStream = fetchDataForModule(session, module, dateFrom); const privacyFilteringStream = new Transform({ objectMode: true, diff --git a/src/index.ts b/src/index.ts index 1913deb..dcb7d46 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { stderr } from "node:process"; import { Command } from "commander"; import { INPUT_PARTY_ID_REGEX, login } from "./commands/login"; import { get } from "./commands/get"; +import parseDate from "./coercion/parseDate"; // send all console messages to stderr so people can pipe OCPI objects over // stdout and see the progress messages written to stdout at the same time @@ -31,12 +32,17 @@ program program .command("get ") .description("Fetch a page of data of a certain OCPI module") + .option( + "--date-from ", + "Limits objects returned to those that have last_updated after or equal to this value", + parseDate + ) .option( "--privacy-pass ", "List of fields to exclude from privacy filtering" ) .action((moduleName: string, options) => - get(moduleName, options["privacyPass"]) + get(moduleName, options["dateFrom"], options["privacyPass"]) ); program.parse(); diff --git a/src/ocpi-request.ts b/src/ocpi-request.ts index b8f8eb6..cde653f 100644 --- a/src/ocpi-request.ts +++ b/src/ocpi-request.ts @@ -55,6 +55,7 @@ export function getModuleByName(moduleName: string): OcpiModule | null { type OcpiPageParameters = { offset: number; limit: number; + date_from?: string; }; export type OcpiResponse = { @@ -209,6 +210,7 @@ const ocpiRequestWithLiteralAuthHeaderTokenValue: ( : { offset: linkToNextPage?.offset, limit: linkToNextPage?.limit, + date_from: linkToNextPage?.date_from }; const ocpiResponse = { ...resp.data, nextPage } as OcpiResponse; @@ -246,7 +248,8 @@ export type NoSuchEndpoint = "no such endpoint"; */ export function fetchDataForModule( session: LoginSession, - module: OcpiModule + module: OcpiModule, + dateFrom?: Date ): Readable { let nextPage: OcpiPageParameters | "done" | "notstarted" = "notstarted"; @@ -264,7 +267,7 @@ export function fetchDataForModule( let nextPageData; try { - const firstPageParameters = { offset: 0, limit: size }; + const firstPageParameters = { offset: 0, limit: size, date_from: dateFrom?.toISOString() }; nextPageData = await pullPageOfData( session, module, @@ -316,10 +319,15 @@ async function pullPageOfData( ); if (moduleUrl) { + let queryUrl = `${moduleUrl.url}?offset=${page.offset}&limit=${page.limit}`; + if (page.date_from) { + queryUrl += `&date_from=${page.date_from}` + } + return ocpiRequest( session, "get", - `${moduleUrl.url}?offset=${page.offset}&limit=${page.limit}`, + queryUrl, fromPartyId ); } else return "no such endpoint";