Skip to content

Commit

Permalink
Node: Add GEOADD (valkey-io#1980)
Browse files Browse the repository at this point in the history
Signed-off-by: TJ Zhang <[email protected]>
Co-authored-by: TJ Zhang <[email protected]>
  • Loading branch information
tjzhang-BQ and TJ Zhang authored Jul 19, 2024
1 parent 5bef4df commit 815e24c
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
* Python: Added transaction supports for DUMP, RESTORE, FUNCTION DUMP and FUNCTION RESTORE ([#1814](https://github.com/valkey-io/valkey-glide/pull/1814))
* Node: Added FlushAll command ([#1958](https://github.com/valkey-io/valkey-glide/pull/1958))
* Node: Added DBSize command ([#1932](https://github.com/valkey-io/valkey-glide/pull/1932))
* Node: Added GeoAdd command ([#1980](https://github.com/valkey-io/valkey-glide/pull/1980))

#### Breaking Changes
* Node: Update XREAD to return a Map of Map ([#1494](https://github.com/valkey-io/valkey-glide/pull/1494))
Expand Down
34 changes: 34 additions & 0 deletions node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
createExists,
createExpire,
createExpireAt,
createGeoAdd,
createGet,
createGetDel,
createHDel,
Expand Down Expand Up @@ -136,6 +137,8 @@ import {
connection_request,
response,
} from "./ProtobufMessage";
import { GeospatialData } from "./commands/geospatial/GeospatialData";
import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions";

/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
type PromiseFunction = (value?: any) => void;
Expand Down Expand Up @@ -3230,6 +3233,37 @@ export class BaseClient {
return this.createWritePromise(createLPos(key, element, options));
}

/**
* Adds geospatial members with their positions to the specified sorted set stored at `key`.
* If a member is already a part of the sorted set, its position is updated.
*
* See https://valkey.io/commands/geoadd/ for more details.
*
* @param key - The key of the sorted set.
* @param membersToGeospatialData - A mapping of member names to their corresponding positions - see
* {@link GeospatialData}. The command will report an error when the user attempts to index
* coordinates outside the specified ranges.
* @param options - The GeoAdd options - see {@link GeoAddOptions}.
* @returns The number of elements added to the sorted set. If `changed` is set to
* `true` in the options, returns the number of elements updated in the sorted set.
*
* @example
* ```typescript
* const options = new GeoAddOptions({updateMode: ConditionalChange.ONLY_IF_EXISTS, changed: true});
* const num = await client.geoadd("mySortedSet", {"Palermo", new GeospatialData(13.361389, 38.115556)}, options);
* console.log(num); // Output: 1 - Indicates that the position of an existing member in the sorted set "mySortedSet" has been updated.
* ```
*/
public geoadd(
key: string,
membersToGeospatialData: Map<string, GeospatialData>,
options?: GeoAddOptions,
): Promise<number> {
return this.createWritePromise(
createGeoAdd(key, membersToGeospatialData, options),
);
}

/**
* @internal
*/
Expand Down
24 changes: 24 additions & 0 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import Long from "long";
import { LPosOptions } from "./commands/LPosOptions";

import { command_request } from "./ProtobufMessage";
import { GeospatialData } from "./commands/geospatial/GeospatialData";
import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions";

import RequestType = command_request.RequestType;

Expand Down Expand Up @@ -1767,3 +1769,25 @@ export function createLPos(
export function createDBSize(): command_request.Command {
return createCommand(RequestType.DBSize, []);
}

/**
* @internal
*/
export function createGeoAdd(
key: string,
membersToGeospatialData: Map<string, GeospatialData>,
options?: GeoAddOptions,
): command_request.Command {
let args: string[] = [key];

if (options) {
args = args.concat(options.toArgs());
}

membersToGeospatialData.forEach((coord, member) => {
args = args.concat(coord.toArgs());
args.push(member);
});

return createCommand(RequestType.GeoAdd, args);
}
28 changes: 28 additions & 0 deletions node/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,12 @@ import {
createZRemRangeByRank,
createZRemRangeByScore,
createZScore,
createGeoAdd,
createFunctionLoad,
} from "./Commands";
import { command_request } from "./ProtobufMessage";
import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions";
import { GeospatialData } from "./commands/geospatial/GeospatialData";

/**
* Base class encompassing shared commands for both standalone and cluster mode implementations in a transaction.
Expand Down Expand Up @@ -1816,6 +1819,31 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
public dbsize(): T {
return this.addAndReturn(createDBSize());
}

/**
* Adds geospatial members with their positions to the specified sorted set stored at `key`.
* If a member is already a part of the sorted set, its position is updated.
*
* See https://valkey.io/commands/geoadd/ for more details.
*
* @param key - The key of the sorted set.
* @param membersToGeospatialData - A mapping of member names to their corresponding positions - see
* {@link GeospatialData}. The command will report an error when the user attempts to index
* coordinates outside the specified ranges.
* @param options - The GeoAdd options - see {@link GeoAddOptions}.
*
* Command Response - The number of elements added to the sorted set. If `changed` is set to
* `true` in the options, returns the number of elements updated in the sorted set.
*/
public geoadd(
key: string,
membersToGeospatialData: Map<string, GeospatialData>,
options?: GeoAddOptions,
): T {
return this.addAndReturn(
createGeoAdd(key, membersToGeospatialData, options),
);
}
}

/**
Expand Down
21 changes: 21 additions & 0 deletions node/src/commands/ConditionalChange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
*/

/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
import { BaseClient } from "src/BaseClient";

/**
* An optional condition to the {@link BaseClient.geoadd} command.
*/
export enum ConditionalChange {
/**
* Only update elements that already exist. Don't add new elements. Equivalent to `XX` in the Valkey API.
*/
ONLY_IF_EXISTS = "XX",

/**
* Only add new elements. Don't update already existing elements. Equivalent to `NX` in the Valkey API.
* */
ONLY_IF_DOES_NOT_EXIST = "NX",
}
52 changes: 52 additions & 0 deletions node/src/commands/geospatial/GeoAddOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
*/

import { ConditionalChange } from "../ConditionalChange";

/**
* Optional arguments for the GeoAdd command.
*
* See https://valkey.io/commands/geoadd/ for more details.
*/
export class GeoAddOptions {
/** Valkey API keyword use to modify the return value from the number of new elements added, to the total number of elements changed. */
public static CHANGED_VALKEY_API = "CH";

private updateMode?: ConditionalChange;

private changed?: boolean;

/**
* Default constructor for GeoAddOptions.
*
* @param updateMode - Options for handling existing members. See {@link ConditionalChange}.
* @param latitude - If `true`, returns the count of changed elements instead of new elements added.
*/
constructor(options: {
updateMode?: ConditionalChange;
changed?: boolean;
}) {
this.updateMode = options.updateMode;
this.changed = options.changed;
}

/**
* Converts GeoAddOptions into a string[].
*
* @returns string[]
*/
public toArgs(): string[] {
const args: string[] = [];

if (this.updateMode) {
args.push(this.updateMode);
}

if (this.changed) {
args.push(GeoAddOptions.CHANGED_VALKEY_API);
}

return args;
}
}
37 changes: 37 additions & 0 deletions node/src/commands/geospatial/GeospatialData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
*/

/**
* Represents a geographic position defined by longitude and latitude.
* The exact limits, as specified by `EPSG:900913 / EPSG:3785 / OSGEO:41001` are the
* following:
*
* Valid longitudes are from `-180` to `180` degrees.
* Valid latitudes are from `-85.05112878` to `85.05112878` degrees.
*/
export class GeospatialData {
private longitude: number;

private latitude: number;

/**
* Default constructor for GeospatialData.
*
* @param longitude - The longitude coordinate.
* @param latitude - The latitude coordinate.
*/
constructor(longitude: number, latitude: number) {
this.longitude = longitude;
this.latitude = latitude;
}

/**
* Converts GeospatialData into a string[].
*
* @returns string[]
*/
public toArgs(): string[] {
return [this.longitude.toString(), this.latitude.toString()];
}
}
113 changes: 113 additions & 0 deletions node/tests/SharedTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import {
} from "./TestUtilities";
import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient";
import { LPosOptions } from "../build-ts/src/commands/LPosOptions";
import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData";
import { GeoAddOptions } from "../build-ts/src/commands/geospatial/GeoAddOptions";
import { ConditionalChange } from "../build-ts/src/commands/ConditionalChange";

async function getVersion(): Promise<[number, number, number]> {
const versionString = await new Promise<string>((resolve, reject) => {
Expand Down Expand Up @@ -4140,6 +4143,116 @@ export function runBaseTests<Context>(config: {
},
config.timeout,
);

it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
`geoadd test_%p`,
async (protocol) => {
await runTest(async (client: BaseClient) => {
const key1 = uuidv4();
const key2 = uuidv4();
const membersToCoordinates = new Map<string, GeospatialData>();
membersToCoordinates.set(
"Palermo",
new GeospatialData(13.361389, 38.115556),
);
membersToCoordinates.set(
"Catania",
new GeospatialData(15.087269, 37.502669),
);

// default geoadd
expect(await client.geoadd(key1, membersToCoordinates)).toBe(2);

// with update mode options
membersToCoordinates.set(
"Catania",
new GeospatialData(15.087269, 39),
);
expect(
await client.geoadd(
key1,
membersToCoordinates,
new GeoAddOptions({
updateMode:
ConditionalChange.ONLY_IF_DOES_NOT_EXIST,
}),
),
).toBe(0);
expect(
await client.geoadd(
key1,
membersToCoordinates,
new GeoAddOptions({
updateMode: ConditionalChange.ONLY_IF_EXISTS,
}),
),
).toBe(0);

// with changed option
membersToCoordinates.set(
"Catania",
new GeospatialData(15.087269, 40),
);
membersToCoordinates.set(
"Tel-Aviv",
new GeospatialData(32.0853, 34.7818),
);
expect(
await client.geoadd(
key1,
membersToCoordinates,
new GeoAddOptions({ changed: true }),
),
).toBe(2);

// key exists but holding non-zset value
expect(await client.set(key2, "foo")).toBe("OK");
await expect(
client.geoadd(key2, membersToCoordinates),
).rejects.toThrow();
}, protocol);
},
config.timeout,
);

it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
`geoadd invalid args test_%p`,
async (protocol) => {
await runTest(async (client: BaseClient) => {
const key = uuidv4();

// empty coordinate map
await expect(client.geoadd(key, new Map())).rejects.toThrow();

// coordinate out of bound
await expect(
client.geoadd(
key,
new Map([["Place", new GeospatialData(-181, 0)]]),
),
).rejects.toThrow();
await expect(
client.geoadd(
key,
new Map([["Place", new GeospatialData(181, 0)]]),
),
).rejects.toThrow();
await expect(
client.geoadd(
key,
new Map([["Place", new GeospatialData(0, 86)]]),
),
).rejects.toThrow();
await expect(
client.geoadd(
key,
new Map([["Place", new GeospatialData(0, -86)]]),
),
).rejects.toThrow();
}, protocol);
},
config.timeout,
);
}

export function runCommonTests<Context>(config: {
Expand Down
Loading

0 comments on commit 815e24c

Please sign in to comment.