Skip to content

Commit

Permalink
Merge pull request #428 from fastrodev/dev
Browse files Browse the repository at this point in the history
feat: add user module
  • Loading branch information
ynwd authored Aug 18, 2024
2 parents f74f217 + eafab32 commit d75a858
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 1 deletion.
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"start": "ENV=DEVELOPMENT deno run --env --unstable-kv -A --watch modules/app/main.ts",
"build": "deno run --env -A --unstable-kv modules/app/main.ts --build ",
"prod": "deno run --env --unstable-kv -A modules/app/main.ts",
"test": "rm -rf .hydrate && rm -rf cov && deno test -A --coverage=cov && deno coverage cov",
"test": "rm -rf .hydrate && rm -rf cov && deno test --unstable-kv -A --coverage=cov && deno coverage cov",
"coverage": "deno coverage cov --lcov > cov.lcov",
"bench": "deno run -A bench/run.ts",
"oauth": "deno run --env -A --unstable-kv examples/oauth.ts",
Expand Down
123 changes: 123 additions & 0 deletions modules/user/user.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { assertEquals } from "@app/http/server/deps.ts";
import {
createUser,
deleteUser,
getUser,
getUserByEmail,
listUsers,
listUsersByEmail,
updateUser,
} from "@app/modules/user/user.service.ts";
import UserType from "@app/modules/user/user.type.ts";
import { collectValues, kv } from "@app/utils/db.ts";

Deno.test({
name: "createUser",
async fn() {
const res = await createUser({
username: "john",
password: "password",
email: "[email protected]",
});
await createUser({
username: "john1",
password: "password",
email: "[email protected]",
});
await createUser({
username: "john3",
password: "password",
email: "[email protected]",
});
await createUser({
username: "john4",
password: "password",
email: "[email protected]",
});
assertEquals(res.ok, true);
},
});

let user: UserType | null;
Deno.test({
name: "getUserByEmail",
async fn() {
user = await getUserByEmail("[email protected]");
assertEquals(user?.email, "[email protected]");
},
});

Deno.test({
name: "updateUser",
async fn() {
if (!user) return;
user.email = "[email protected]";
if (user.id) {
const res = await updateUser(user?.id, user);
assertEquals(res?.ok, true);
}
},
});

Deno.test({
name: "getUser",
async fn() {
if (user?.id) {
user = await getUser(user?.id);
}
assertEquals(user?.email, "[email protected]");
},
});

Deno.test({
name: "listUsers",
async fn() {
const res = await collectValues(listUsers());
assertEquals(res.length, 4);
},
});

Deno.test({
name: "listUsers",
async fn() {
const iterator = listUsers({ limit: 1 });
const res = await collectValues(iterator);
assertEquals(res.length, 1);

const iter2 = listUsers({ limit: 1, cursor: iterator.cursor });
const res2 = await collectValues(iter2);
assertEquals(res2.length, 1);

const iter3 = listUsers({ limit: 1, cursor: iterator.cursor });
const res3 = await collectValues(iter3);
assertEquals(res3.length, 1);
},
});

Deno.test({
name: "listUsersByEmail",
async fn() {
const res = await collectValues(listUsersByEmail());
assertEquals(res.length, 4);
},
});

Deno.test({
name: "deleteUser",
async fn() {
if (user?.id) {
const res = await deleteUser(user.id);
assertEquals(res?.ok, true);
}
},
});

Deno.test({
name: "reset",
async fn() {
const iter = kv.list({ prefix: [] });
const promises = [];
for await (const res of iter) promises.push(kv.delete(res.key));
await Promise.all(promises);
},
});
77 changes: 77 additions & 0 deletions modules/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { ulid } from "jsr:@std/ulid";
import { kv } from "@app/utils/db.ts";
import UserType from "@app/modules/user/user.type.ts";

export async function getUserByEmail(email: string) {
const res = await kv.get<UserType>(["users_by_email", email]);
return res.value;
}

export async function getUser(id: string): Promise<UserType | null> {
const res = await kv.get<UserType>(["users", id]);
return res.value;
}

export function listUsers(
options?: Deno.KvListOptions,
) {
return kv.list<UserType>({ prefix: ["users"] }, options);
}

export function listUsersByEmail(
options?: Deno.KvListOptions,
) {
return kv.list<UserType>({ prefix: ["users_by_email"] }, options);
}

export async function createUser(user: UserType) {
user.id = user.id ? user.id : ulid();
const primaryKey = ["users", user.id];
const byEmailKey = ["users_by_email", user.email];

const res = await kv.atomic()
.check({ key: primaryKey, versionstamp: null })
.check({ key: byEmailKey, versionstamp: null })
.set(primaryKey, user)
.set(byEmailKey, user)
.commit();

if (!res.ok) {
throw new TypeError("User with ID or email already exists");
}

return res;
}

export async function updateUser(id: string, user: UserType) {
if (!id) return;
const existingUser = await kv.get<UserType>(["users", id]);
if (!existingUser.value?.email) return;

const byEmailKey = ["users_by_email", user.email];
const atomicOp = kv.atomic()
.check(existingUser)
.delete(["users_by_email", existingUser.value?.email])
.set(["users", id], user)
.check({ key: byEmailKey, versionstamp: null })
.set(byEmailKey, user);

const res = await atomicOp.commit();
if (!res.ok) throw new Error("Failed to update user");
return res;
}

export async function deleteUser(id: string) {
let res = { ok: false };
while (!res.ok) {
const getRes = await kv.get<UserType>(["users", id]);
if (getRes && getRes.value) {
res = await kv.atomic()
.check(getRes)
.delete(["users", id])
.delete(["users_by_email", getRes.value.email])
.commit();
}
}
return res;
}
10 changes: 10 additions & 0 deletions modules/user/user.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
type UserType = {
id?: string;
username: string;
email: string;
password: string;
group?: string[];
image?: string;
};

export default UserType;
13 changes: 13 additions & 0 deletions task/db_dump.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { kv } from "@app/utils/db.ts";

function replacer(_key: unknown, value: unknown) {
return typeof value === "bigint" ? value.toString() : value;
}

const items = await Array.fromAsync(
kv.list({ prefix: [] }),
({ key, value }) => ({ key, value }),
);
console.log(JSON.stringify(items, replacer, 2));

kv.close();
8 changes: 8 additions & 0 deletions task/db_reset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { kv } from "@app/utils/db.ts";
if (!confirm("WARNING: The database will be reset. Continue?")) Deno.exit();
const iter = kv.list({ prefix: [] });
const promises = [];
for await (const res of iter) promises.push(kv.delete(res.key));
await Promise.all(promises);

kv.close();
4 changes: 4 additions & 0 deletions utils/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ async function getKvInstance(path?: string): Promise<Deno.Kv> {
}

export const kv = await getKvInstance(path);

export async function collectValues<T>(iter: Deno.KvListIterator<T>) {
return await Array.fromAsync(iter, ({ value }) => value);
}

0 comments on commit d75a858

Please sign in to comment.