Skip to content
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

Node: Add GEOADD #1980

Merged
merged 1 commit into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading