Skip to content

Commit

Permalink
switch to using redisSearch. Hope it exists on the server!
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisrude committed Aug 13, 2023
1 parent 446ffb5 commit a088b69
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 47 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"REDISUSER",
"resave",
"rudesoftware",
"Subkey",
"Ungroup",
"unmark",
"uuidv"
Expand Down
10 changes: 7 additions & 3 deletions drumline-client/src/lib/network/puzzle_rest_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,23 @@ const HEADERS: HeadersInit = {
'Content-Type': 'application/json'
};

// login and logout
type PuzzleListInfo = {
puzzle_id: string,
size: number,
your_puzzle: boolean,
};

//////

// app.get('/puzzles', this.list_puzzles);
// 200 - ok
// res.send({
// result: 'OK',
// puzzle_ids
// puzzle_list: [PuzzleListInfo]
// });
type PuzzleListResponse = {
result: 'OK';
puzzle_ids: string[];
puzzle_ids: PuzzleListInfo[];
};
const puzzles_list = async (
user_id: UserId,
Expand Down
4 changes: 2 additions & 2 deletions drumline-server/src/express/puzzle-crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ class PuzzleCrudder {
res.status(401).send(`No user ID`);
return;
}
const puzzle_ids = await this._redis_client.listMyPuzzles(user.private_uuid);
const puzzle_list = await this._redis_client.listMyPuzzles(user.private_uuid);
res.status(200).send({
result: 'OK',
puzzle_ids
puzzle_list
});
};

Expand Down
195 changes: 153 additions & 42 deletions drumline-server/src/redis/puzzle_redis_client.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,41 @@
import { Puzzle, loadPuzzleFromJson } from '@chrisrude/drumline-lib';
import { RedisClientType, createClient } from 'redis';
import redisSearch from '@redis/search';
import { RedisClientType, SchemaFieldTypes, createClient } from 'redis';

const RESULT_OK = 'OK';

const AUTHOR_INDEX = 'idx:author';
const SIZE_INDEX = 'idx:size';

const PUZZLE_KEY_PREFIX = "puzzle";
const PUZZLE_INPUT_SUBKEY = 'input';
const PUZZLE_AUTHOR_SUBKEY = 'author_uuid';
const PUZZLE_SIZE_SUBKEY = 'size';
const PUZZLE_CREATED_AT_SUBKEY = 'created_at';


export { PuzzleRedisClient };
export type { PuzzleListInfo };

// todo: move to lib?
type PuzzleListInfo = {
puzzle_id: string,
size: number,
your_puzzle: boolean,
};

type PuzzleSearchResult = {
PUZZLE_INPUT_SUBKEY: string,
PUZZLE_AUTHOR_SUBKEY: string,
PUZZLE_SIZE_SUBKEY: number,
}

const modules = {
redisSearch
};

class PuzzleRedisClient {
private readonly _client: RedisClientType;
private readonly _client: RedisClientType<typeof modules>;

constructor(
url: string | undefined,
Expand All @@ -17,12 +46,61 @@ class PuzzleRedisClient {
socket: {
connectTimeout: 50000,
},
modules,
});
}

_create_index = async (): Promise<void> => {
const modules = await this._client.moduleList();
console.log(`modules: `, modules);
const index_names = await this._client.redisSearch._list();
if (index_names.includes(AUTHOR_INDEX)) {
console.log(`index ${AUTHOR_INDEX} already exists`);
} else {
console.log(`creating index ${AUTHOR_INDEX}`);
const result1 = await this._client.redisSearch.create(
AUTHOR_INDEX,
{
PUZZLE_AUTHOR_SUBKEY: {
type: SchemaFieldTypes.TAG,
}
},
{
ON: 'HASH',
PREFIX: PUZZLE_KEY_PREFIX
}
);
if (result1 !== RESULT_OK) {
throw new Error(`create index returned ${result1}`);
}
}
if (index_names.includes(SIZE_INDEX)) {
console.log(`index ${SIZE_INDEX} already exists`);
} else {
console.log(`creating index ${SIZE_INDEX}`);
const result = await this._client.redisSearch.create(
SIZE_INDEX,
{
PUZZLE_SIZE_SUBKEY: {
type: SchemaFieldTypes.NUMERIC,
SORTABLE: true,
}
},
{
ON: 'HASH',
PREFIX: PUZZLE_KEY_PREFIX
});
if (result !== RESULT_OK) {
throw new Error(`create index returned ${result}`);
}
};
}

connect = async (): Promise<void> => {
console.log(`connecting to redis...`);
return this._client.connect();
const result = this._client.connect();
this._create_index();
return result;
};

disconnect = async (): Promise<void> => {
Expand All @@ -31,31 +109,33 @@ class PuzzleRedisClient {

// saves the puzzle to redis as a new solve attempt, and returns
// the key to use to retrieve it later.
savePuzzle = async (puzzle: Puzzle, puzzle_id: string, creator_uuid: string): Promise<void> => {
savePuzzle = async (puzzle: Puzzle, puzzle_id: string, author_uuid: string): Promise<void> => {
const puzzle_key = this._puzzleKey(puzzle_id);
const puzzle_author_key = this._puzzleCreatorKey(creator_uuid);

const results = await this._client.multi().set(
puzzle_key, puzzle.original_text
).sAdd(
puzzle_author_key, puzzle_key
).exec();
const results = await this._client.multi()
.hSet(puzzle_key, PUZZLE_INPUT_SUBKEY, puzzle.original_text)
.hSet(puzzle_key, PUZZLE_AUTHOR_SUBKEY, author_uuid)
.hSet(puzzle_key, PUZZLE_SIZE_SUBKEY, puzzle.size)
.hSet(puzzle_key, PUZZLE_CREATED_AT_SUBKEY, Date.now())
.exec();

// second value will be 1 if we added the key, 0 if it already existed,
// which is ok either way
// result value will be 1 if the key didn't exist before,
// and 0 if it did. Either is ok.
if (!results ||
!results.length ||
results.length !== 2 ||
results[0] !== RESULT_OK ||
(results[1] !== 1 && results[1] !== 0)
) {
results.length !== 4 ||
(results[0] !== 0 && results[0] !== 1) ||
(results[1] !== 0 && results[1] !== 1) ||
(results[2] !== 0 && results[2] !== 1) ||
(results[3] !== 0 && results[3] !== 1)) {
throw new Error(`results of multi set for puzzle is ${results} `);
}
};

loadPuzzle = async (puzzle_id: string): Promise<Puzzle | null> => {
const results = await this._client.get(
this._puzzleKey(puzzle_id)
const results = await this._client.hGet(
this._puzzleKey(puzzle_id),
PUZZLE_INPUT_SUBKEY,
);
if (!results) {
// puzzle not found
Expand All @@ -68,46 +148,77 @@ class PuzzleRedisClient {
return puzzle;
};

listMyPuzzles = async (creator_uuid: string): Promise<string[]> => {
const puzzle_author_key = this._puzzleCreatorKey(creator_uuid);
const results = await this._client.sMembers(puzzle_author_key);
return results ?? [];
};
listMyPuzzles = async (author_uuid: string): Promise<PuzzleListInfo[]> => {
const results = await this._client.redisSearch.search(
AUTHOR_INDEX,
`@${PUZZLE_AUTHOR_SUBKEY}:{${author_uuid}}`,
)
if (!results) {
throw new Error(`search for author ${author_uuid} returned null`);
}

// results should look like:
// {
// total: 2,
// documents: [
// {
// id: 'noderedis:animals:4',
// value: {
// name: 'Fido',
// species: 'dog',
// age: '7'
// }
// },
// {
// id: 'noderedis:animals:3',
// value: {
// name: 'Rover',
// species: 'dog',
// age: '9'
// }
// }
// ]
// }
return results.documents.map((result) => {
const puzzle_id = this._puzzleIdFromKey(result.id);
const search_result = result.value as PuzzleSearchResult;
return {
puzzle_id,
size: search_result.PUZZLE_SIZE_SUBKEY,
your_puzzle: (search_result.PUZZLE_AUTHOR_SUBKEY === author_uuid),
};
});
}

// returns true if delete key existed and was deleted,
// false if it did not exist
deletePuzzle = async (puzzle_id: string, requester_uuid: string): Promise<boolean> => {
// test that the puzzle author is the same as the user_id who
// wants to delete the puzzle,
// and if so delete both the puzzle and the author entry
const puzzle_author_key = this._puzzleCreatorKey(requester_uuid);
const puzzle_key = this._puzzleKey(puzzle_id);

await this._client.watch(puzzle_author_key);
const author_id_matches = await this._client.sIsMember(puzzle_author_key, puzzle_key);
if (!author_id_matches) {
// key did not exist, must have been deleted already
// or just was never created
await this._client.watch(puzzle_key);
const author_uuid = await this._client.hGet(puzzle_key, PUZZLE_AUTHOR_SUBKEY);

if (author_uuid !== requester_uuid) {
// either puzzle did not exist or requester is not the author
await this._client.unwatch();
return false;
}

const results = await this._client.multi().del(
puzzle_key
).sRem(
puzzle_author_key, puzzle_key
).exec();
if (!results || !results.length || results.length !== 2 || results[0] !== RESULT_OK || results[1] !== 1) {
throw new Error(`delete returned unexpected: ${results}`);
const del_result = await this._client.multi().del(puzzle_key).exec();
if (!del_result || del_result.length !== 1 || del_result[0] !== RESULT_OK) {
throw new Error(`delete returned unexpected: ${del_result}`);
}

return true;
};

_puzzleKey = (puzzle_id: string): string => {
return `puzzle:${puzzle_id}:input_text`;
};
_puzzleCreatorKey = (creator_private_uuid: string): string => {
return `author:${creator_private_uuid}:puzzles`;
};
_puzzleKey = (puzzle_id: string): string =>
`${PUZZLE_KEY_PREFIX}:${puzzle_id}`;

_puzzleIdFromKey = (puzzle_key: string): string =>
puzzle_key.substring(PUZZLE_KEY_PREFIX.length + 1);

}

0 comments on commit a088b69

Please sign in to comment.