initial commit ??

This commit is contained in:
bootunloader
2024-09-11 02:57:43 +03:00
commit 33cbeaa9a3
186 changed files with 17269 additions and 0 deletions

3
.dockerignore Executable file
View File

@@ -0,0 +1,3 @@
node_modules
build
.env

4
.gitignore vendored Executable file
View File

@@ -0,0 +1,4 @@
node_modules
build
.env
.svelte-kit

19
Dockerfile Executable file
View File

@@ -0,0 +1,19 @@
FROM node:18-alpine as builder
RUN apk add --no-cache libc6-compat
RUN npm i -g pnpm
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install
COPY . .
RUN pnpm run build
EXPOSE 80
CMD ["pnpm", "run", "start"]

15
Dockerfile.bun Executable file
View File

@@ -0,0 +1,15 @@
FROM oven/bun:1.0.0
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install
COPY . .
RUN bun run build
EXPOSE 80
CMD ["bun", "run", "start"]

14
components.json Normal file
View File

@@ -0,0 +1,14 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app.css",
"baseColor": "gray"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
},
"typescript": true
}

67
package.json Executable file
View File

@@ -0,0 +1,67 @@
{
"name": "rdv",
"description": "",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"start": "HOST=0.0.0.0 PORT=80 node ./build/index.js",
"build": "pnpm run check && vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"test:unit": "vitest --watch"
},
"dependencies": {
"@melt-ui/svelte": "^0.22.2",
"@sveltejs/svelte-virtual-list": "^3.0.1",
"@tanstack/match-sorter-utils": "^8.8.4",
"@tanstack/svelte-query": "^4.35.3",
"@tanstack/svelte-table": "^8.9.4",
"@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2",
"add": "^2.0.6",
"bcryptjs": "^2.4.3",
"bits-ui": "^0.21.13",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"date-fns-tz": "^3.1.3",
"dayjs": "^1.11.9",
"dotenv": "^16.3.1",
"https-proxy-agent": "^7.0.5",
"ioredis": "^5.3.2",
"lucide-svelte": "^0.424.0",
"node-fetch": "^3.3.2",
"surrealdb.js": "^0.8.2",
"svelte-french-toast": "^1.1.0",
"svelte-headlessui": "^0.0.20",
"tailwind-merge": "^2.4.0",
"tailwind-variants": "^0.2.1",
"trpc-svelte-query-adapter": "^2.1.0",
"trpc-sveltekit": "^3.6.2",
"ulid": "^2.3.0",
"uuid": "^9.0.0",
"vite": "^5.3.5",
"zod": "^3.21.4"
},
"devDependencies": {
"@iconify/json": "^2.2.91",
"@sveltejs/adapter-auto": "^2.1.0",
"@sveltejs/adapter-node": "^1.3.1",
"@sveltejs/kit": "^1.22.3",
"@tailwindcss/typography": "^0.5.13",
"@types/bcryptjs": "^2.4.2",
"@types/node": "^20.4.2",
"@types/uuid": "^9.0.2",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"svelte": "^4.0.5",
"svelte-check": "^3.4.6",
"tailwindcss": "^3.4.4",
"tslib": "^2.6.0",
"typescript": "^5.1.6",
"unplugin-auto-import": "^0.16.6",
"unplugin-icons": "^0.16.5",
"vitest": "^0.33.0"
}
}

3543
pnpm-lock.yaml generated Executable file

File diff suppressed because it is too large Load Diff

6
postcss.config.js Executable file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

View File

@@ -0,0 +1,57 @@
import { expect, test } from "vitest";
import {
generatePostDataArrayFromBaseInfo,
rebalancePostDataListByBalanceOfUsers,
} from "$lib/server/postdata/postdata.gen.controller";
import type { ApiPostUser, PostDataEntry } from "$lib/utils/data.types";
import { buildMessageString } from "$lib/server/postdata/post.handler";
test("test rebalancePostDataListByBalanceOfUsers", () => {
// empty case
const testA = {} as Record<string, number>;
const testB = [] as any[];
const testC = [] as any[];
rebalancePostDataListByBalanceOfUsers(testA, testB, testC);
expect(testA).toEqual({});
expect(testB).toEqual([]);
expect(testC).toEqual([]);
const balanceCounts: Record<string, number> = {};
const users: ApiPostUser[] = [];
const result: PostDataEntry[] = [];
});
test("post body message string generation function", () => {
let i = 0,
data = [] as PostDataEntry[],
distId = 1,
dealerId = 2,
drawId = 69,
date = new Date().toISOString().split("T")[0];
let out = buildMessageString(i, data, distId, dealerId, drawId, date);
expect(out.message).toEqual("");
// data.push({
// id: "123123",
// first: 10,
// second: 20,
// number: "1234",
// });
// out = buildMessageString(i, data, distId, dealerId, drawId, date);
// expect(out.message).toEqual(`1,1,2,${drawId},${date},1234,10,20,30`);
// data.push({
// id: "123124",
// first: 0,
// second: 5,
// number: "0987",
// });
// out = buildMessageString(i, data, distId, dealerId, drawId, date);
// expect(out.message).toEqual(
// `1,1,2,${drawId},${date},1234,10,20,30;2,1,2,${drawId},${date},0987,0,5,5`,
// );
});

90
src/app.css Executable file
View File

@@ -0,0 +1,90 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: "Inter";
src: url("/fonts/InterV.ttf");
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 224 71.4% 4.1%;
--muted: 220 14.3% 95.9%;
--muted-foreground: 220 8.9% 46.1%;
--popover: 0 0% 100%;
--popover-foreground: 224 71.4% 4.1%;
--card: 0 0% 100%;
--card-foreground: 224 71.4% 4.1%;
--border: 220 13% 91%;
--input: 220 13% 91%;
--primary: 220.9 39.3% 11%;
--primary-foreground: 210 20% 98%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
--accent: 220 14.3% 95.9%;
--accent-foreground: 220.9 39.3% 11%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 210 20% 98%;
--ring: 224 71.4% 4.1%;
--radius: 0.5rem;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-family:
"Inter",
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
}
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0;
/* <-- Apparently some margin are still there even though it's hidden */
}
input[type="number"] {
-moz-appearance: textfield;
/* Firefox */
}
select {
/* for Firefox */
-moz-appearance: none;
/* for Safari, Chrome, Opera */
-webkit-appearance: none;
}
select::-ms-expand {
display: none;
}

18
src/app.d.ts vendored Executable file
View File

@@ -0,0 +1,18 @@
/// <reference types="@sveltejs/kit" />
/// <reference types="unplugin-icons/types/svelte" />
import type { SessionData } from "$lib/server/cookie.functions";
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
interface Locals {
user?: SessionData;
}
// interface PageData {}
// interface Platform {}
}
}
export { };

15
src/app.html Executable file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

22
src/auto-imports.d.ts vendored Executable file
View File

@@ -0,0 +1,22 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const afterUpdate: typeof import('svelte')['afterUpdate']
const beforeUpdate: typeof import('svelte')['beforeUpdate']
const createEventDispatcher: typeof import('svelte')['createEventDispatcher']
const first: typeof import('./hooks.server')['first']
const getAllContexts: typeof import('svelte')['getAllContexts']
const getContext: typeof import('svelte')['getContext']
const handle: typeof import('./hooks.server')['handle']
const handleError: typeof import('./hooks.server')['handleError']
const hasContext: typeof import('svelte')['hasContext']
const onDestroy: typeof import('svelte')['onDestroy']
const onMount: typeof import('svelte')['onMount']
const second: typeof import('./hooks.server')['second']
const setContext: typeof import('svelte')['setContext']
const tick: typeof import('svelte')['tick']
}

66
src/hooks.server.ts Executable file
View File

@@ -0,0 +1,66 @@
import type { Handle, HandleServerError } from "@sveltejs/kit";
import {
getParsedSession,
parseCookieString,
type SessionData,
} from "$lib/server/cookie.functions";
import { createContext } from "$lib/trpc/context";
import { router } from "$lib/trpc/router";
import { createTRPCHandle } from "trpc-sveltekit";
import { sequence } from "@sveltejs/kit/hooks";
export const handleError: HandleServerError = async ({ error, event }) => {
console.log("[-] Running error middleware for : ", event.url.pathname);
console.log(error);
return {
message: (error as Error).message,
};
};
export const first: Handle = createTRPCHandle({ router, createContext });
export const second: Handle = async ({ resolve, event }) => {
if (
event.url.pathname.includes("/api/auth") ||
event.url.pathname.includes("/api/debug") ||
event.url.pathname.includes("/trpc") ||
event.url.pathname.includes("/auth")
) {
return await resolve(event);
}
console.log("[+] Running hook middleware for : ", event.url.pathname);
const sessionId = parseCookieString(
event.request.headers.get("cookie") || "",
).SID;
const baseUrl = event.url.origin;
const signInUrl = baseUrl + "/auth/signin";
const isSignInPage = event.url.pathname === "/auth/signin";
const redirectResponse = new Response(null, {
status: 302,
headers: {
Location: signInUrl,
"Set-Cookie": sessionId
? "SID=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT"
: "",
},
});
if (!sessionId && !isSignInPage) {
return redirectResponse;
}
const user = await getParsedSession(sessionId);
if (!user && !isSignInPage) {
return redirectResponse;
} else if (user && isSignInPage) {
return new Response(null, {
status: 302,
headers: {
Location: baseUrl,
},
});
}
event.locals.user = (user ?? undefined) as SessionData | undefined;
// optional: make this a variable to run code after the request is prepared
return await resolve(event);
};
export const handle = sequence(first, second);

7
src/index.test.ts Executable file
View File

@@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

View File

@@ -0,0 +1,35 @@
import clsx from "clsx";
import { tv } from "tailwind-variants";
export const buttonStyles = tv(
{
base: clsx(
"p-2 px-4 flex gap-1.5 items-center justify-center rounded-md transition-all duration-150 shadow-sm font-medium tracking-wide",
),
variants: {
intent: {
primary: clsx("bg-sky-500 text-white hover:bg-sky-600"),
secondary: "bg-sky-800 text-white",
success: "bg-emerald-500 text-white hover:bg-emerald-600",
danger: "bg-rose-500 text-white hover:bg-rose-600",
warning: "bg-amber-500 text-white hover:bg-amber-600",
ghost: clsx(
"bg-slate-100 text-slate-500 hover:text-slate-600 hover:bg-slate-200",
),
primaryInverted: clsx("bg-sky-50 text-sky-600 hover:bg-sky-100"),
successInverted: clsx(
"bg-emerald-50 hover:bg-emerald-100 text-emerald-600",
),
dangerInverted: clsx("bg-slate-100 hover:bg-slate-200 text-rose-600"),
warningInverted: clsx("bg-amber-50 hover:bg-amber-100 text-amber-600"),
},
size: { sm: "text-sm", md: "text-md", lg: "text-lg" },
fullwidth: { yes: "w-full", max: "w-max", no: "" },
},
defaultVariants: {
intent: "primary",
size: "sm",
},
},
{},
);

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import type { SvelteComponent } from "svelte";
import { buttonStyles } from "./button.styles";
export let fullwidth: keyof typeof buttonStyles.variants.fullwidth = "no";
export let intent: keyof typeof buttonStyles.variants.intent = "primary";
export let text: string;
export let iconleft: typeof SvelteComponent | undefined = undefined;
export let iconright: typeof SvelteComponent | undefined = undefined;
export let otherOptions: any = {};
export let disabled: boolean = false;
export let onClick: (e: Event) => void = () => {};
</script>
<button
{...otherOptions}
on:click={onClick}
class={buttonStyles({ intent: intent, fullwidth: fullwidth })}
{disabled}
>
{#if iconleft}
<svelte:component this={iconleft} size={24} />
{/if}
<span class="break-keep">
{text}
</span>
{#if iconright}
<svelte:component this={iconright} size={24} />
{/if}
</button>

View File

@@ -0,0 +1,43 @@
<script lang="ts">
import { randomString } from "$lib/utils";
import { COLOR_TRANSITION } from "$lib/utils/constants";
import { createCheckbox } from "@melt-ui/svelte";
import clsx from "clsx";
import IconMdiCheck from "~icons/mdi/check";
import IconIcOutlineMinus from "~icons/ic/outline-minus";
export let checked: boolean | "indeterminate";
export let disabled: boolean = false;
export let onChange: (checked: boolean | "indeterminate") => void = () => {};
const {
root,
input,
isChecked,
isIndeterminate,
checked: checkedStore,
} = createCheckbox({ checked, disabled });
checkedStore.subscribe((v) => {
onChange(v);
});
const inputId = randomString(16);
</script>
<form>
<button
{...$root}
class={clsx(
"flex h-6 w-6 appearance-none items-center justify-center rounded-sm bg-white text-sky-600 shadow-lg hover:bg-slate-50 border border-slate-300 hover:border-sky-500",
COLOR_TRANSITION
)}
id={inputId}
use:root
>
{#if $isIndeterminate}
<IconIcOutlineMinus />
{:else if $isChecked}
<IconMdiCheck />
{/if}
<input {...$input} />
</button>
</form>

View File

@@ -0,0 +1,48 @@
<script lang="ts">
import clsx from "clsx";
import type { SvelteComponent } from "svelte";
const colors = {
gray: "bg-gray-200 text-gray-600 hover:bg-gray-300",
blue: "bg-blue-100 text-blue-500 hover:bg-blue-200 hover:text-blue-600",
amber:
"bg-amber-100 text-amber-500 hover:bg-amber-200 hover:text-amber-600",
green:
"bg-green-100 text-green-500 hover:bg-green-200 hover:text-green-600",
sky:
"bg-sky-100 text-sky-500 hover:bg-sky-200 hover:text-sky-600",
red: "bg-red-100 text-red-500 hover:bg-red-200 hover:text-red-600",
} as const;
export let icon: typeof SvelteComponent | undefined = undefined;
export let color: keyof typeof colors = "sky";
export let size: string = "8";
export let onClick: () => void = () => {};
export let disabled: boolean = false;
export let position: "bottomleft" | "bottomright" | "topleft" | "topright" =
"bottomright";
export let hideOnDesktop: boolean = false;
const onClickHandler = () => {
if (onClick && !disabled) {
onClick();
}
};
</script>
<button
type="button"
class={clsx(
"flex w-max cursor-pointer appearance-none items-center gap-2 rounded-lg p-3 shadow-lg transition-colors duration-150",
colors[color ?? "sky"],
disabled && "opacity-50",
position === "bottomleft" && "fixed bottom-4 left-4",
position === "bottomright" && "fixed bottom-4 right-4",
position === "topleft" && "fixed left-4 top-4",
position === "topright" && "fixed right-4 top-4",
hideOnDesktop && "lg:hidden",
)}
on:click={onClickHandler}
>
<svelte:component this={icon} class={clsx(`h-${size} w-${size}`)} />
</button>

View File

@@ -0,0 +1,107 @@
<script lang="ts">
import clsx from "clsx";
import { createListbox } from "svelte-headlessui";
import IconCheck from "~icons/bi/check";
import IconSelector from "~icons/majesticons/selector-line";
import { randomString } from "$lib/utils";
type Option = {
id?: string | number;
label: string;
value: string;
};
export let componentId: string = randomString(10);
export let label: string = "";
export let options: Option[];
export let multiple: boolean = false;
export let selected: Option[] = [];
export let onSelect: (option: Option) => void = () => {};
export let fullWidth: boolean = false;
const listbox = createListbox({
label: "Actions",
});
const setToSelected = (option: Option) => {
if (multiple) {
const alreadyPresent = selected.find(
(item) => item.value === option.value
);
if (alreadyPresent) {
selected = selected.filter((item) => item.value !== option.value);
return;
}
selected.push(option);
selected = selected;
} else {
selected = [option];
}
};
function _onSelect(e: Event) {
const { selected } = (e as CustomEvent).detail;
setToSelected(selected);
onSelect(selected);
}
</script>
<div class={clsx("relative flex flex-col gap-1", fullWidth && "w-full")}>
<label class="mb-1 pl-1 text-sm" for={componentId}>{label}</label>
<button
id={componentId}
use:listbox.button
on:select={_onSelect}
class={clsx(
"rounded-md relative cursor-pointer border text-left border-gray-300 outline-none p-2 transition-colors duration-200 bg-slate-50 hover:bg-slate-50 hover:border-black focus:border-sky-500 focus:bg-sky-50 focus:placeholder:text-sky-400 focus:text-sky-700",
fullWidth ? "w-full" : "w-max"
)}
>
<span class="block truncate"
>{selected.length && selected[0].label.length > 0
? selected[0].label
: "Select an option"}</span
>
<span
class="pointer-events-none absolute z-50 inset-y-0 right-0 flex items-center pr-2"
>
<IconSelector class="h-5 w-5 text-gray-400" />
</span>
</button>
<div class={clsx($listbox.expanded ? "block" : "hidden")}>
<ul
use:listbox.items
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
{#each options as value}
{@const chosen = selected.find((item) => item.value === value.value)}
{@const active = $listbox.active === value}
<li
class={clsx(
"duration-50 relative cursor-pointer select-none py-2 pl-10 pr-4 text-slate-800 transition-colors hover:bg-sky-100",
active ? "bg-sky-100" : ""
)}
use:listbox.item={{ value }}
>
<span
class={clsx(
"block truncate",
chosen ? "font-medium" : "font-normal"
)}>{value.label}</span
>
{#if chosen}
<span
class={clsx(
active ? "text-white" : "text-sky-600",
"absolute inset-y-0 left-0 flex items-center pl-3 text-sky-600 hover:text-white"
)}
>
<IconCheck class="h-6 w-6" />
</span>
{/if}
</li>
{/each}
</ul>
</div>
</div>

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import clsx from "clsx";
import type { SvelteComponent } from "svelte";
const colors = {
gray: "bg-gray-200 text-gray-600 hover:bg-gray-300",
blue: "bg-blue-50 text-blue-500 hover:bg-blue-100",
amber: "bg-amber-50 text-amber-500 hover:bg-amber-100",
green: "bg-green-50 text-green-500 hover:bg-green-100",
sky: "bg-sky-50 text-sky-500 hover:bg-sky-100",
red: "bg-red-50 text-red-500 hover:bg-red-100",
} as const;
export let icon: typeof SvelteComponent | undefined = undefined;
export let color: keyof typeof colors = "sky";
export let size: string = "8";
export let onClick: () => void = () => {};
export let disabled: boolean = false;
const onClickHandler = () => {
if (onClick && !disabled) {
onClick();
}
};
</script>
<button
type="button"
class={clsx(
"w-max cursor-pointer rounded-lg p-2 transition-colors duration-150",
colors[color],
)}
on:click={onClickHandler}
>
<svelte:component this={icon} class={clsx(`h-${size} w-${size}`)} />
</button>

View File

@@ -0,0 +1,143 @@
<script lang="ts">
import clsx from "clsx";
import type { SvelteComponent } from "svelte";
import { tv } from "tailwind-variants";
import { randomString } from "$lib/utils";
export let name: string = "";
export let inputType: string = "text";
export let placeholder: string = "";
export let value: string | number = "";
export let disabled: boolean = false;
export let hidden: boolean = false;
export let intent: keyof typeof baseFormFieldStyles.variants.intent =
"primary";
export let bordered: keyof typeof baseFormFieldStyles.variants.bordered =
"none";
export let padding: keyof typeof baseFormFieldStyles.variants.padding =
"default";
export let fieldWidth: "max" | "full" = "full";
export let label: string = "";
export let labelicon: typeof SvelteComponent | undefined = undefined;
export let iconleft: typeof SvelteComponent | undefined = undefined;
export let iconright: typeof SvelteComponent | undefined = undefined;
export let bottomLabel: string = "";
export let isError: boolean = false;
export let onInput: (e: Event) => void = () => {};
export let otherInputOptions: Record<string, string | number | boolean> =
{};
const baseFormFieldStyles = tv({
base: clsx(
"rounded-md border border-gray-300 outline-none cursor-text transition-colors duration-200",
),
variants: {
intent: {
primary: clsx(
"bg-slate-50 hover:bg-slate-50 hover:border-black focus:border-sky-500 focus:bg-sky-50 focus:placeholder:text-sky-400 focus:text-sky-700",
),
danger: clsx(
"bg-red-50 border-red-500 hover:bg-red-100 hover:border-red-700 placeholder:text-rose-400 focus:text-red-700",
),
success: clsx(
"bg-green-50 border-green-500 hover:bg-green-100 hover:border-green-700 placeholder:text-green-400 focus:text-green-700",
),
},
bordered: {
none: "",
yes: "",
},
padding: {
default: "p-2",
sm: "px-2 p-1.5",
},
horizontalPadding: {
none: "",
md: "px-4",
lg: "px-8",
},
disabled: {
none: "",
yes: "cursor-not-allowed bg-slate-200/80",
},
len: { max: "w-max", full: "w-full" },
},
defaultVariants: {
intent: "primary",
len: "full",
disabled: "none",
padding: "default",
},
});
const typeAction = (node: HTMLInputElement) => {
node.type = inputType;
};
const iconCommonStyle = clsx(
"w-6 h-6",
isError
? "text-red-800"
: intent === "success"
? "text-green-700"
: "text-gray-500",
);
const inputId = randomString(16);
</script>
<div
class={clsx(
"flex flex-col gap-2",
fieldWidth === "full" ? "w-full" : "w-max",
hidden ? "hidden" : "",
)}
>
{#if label.length > 0}
<div class="gap flex items-center pl-2 text-black">
{#if labelicon}
<svelte:component this={labelicon} class={"h-4 w-4"} />
{/if}
<label for={inputId} class="text-sm">{label}</label>
</div>
{/if}
<div class="relative">
{#if iconleft}
<svelte:component
this={iconleft}
class={clsx("absolute left-2 top-2.5", iconCommonStyle)}
/>
{/if}
<input
{name}
use:typeAction
{placeholder}
bind:value
on:input={onInput}
class={clsx(
baseFormFieldStyles({
intent: isError ? "danger" : intent,
bordered: bordered,
len: fieldWidth,
horizontalPadding: iconleft || iconright ? "lg" : "none",
disabled: disabled === true ? "yes" : "none",
padding: padding,
}),
)}
disabled={disabled ?? false}
id={inputId}
{...otherInputOptions}
/>
{#if iconright}
<svelte:component
this={iconright}
class={clsx("absolute right-2 top-3", iconCommonStyle)}
/>
{/if}
</div>
{#if bottomLabel.length > 0}
{#if isError}
<small class="text-xs text-red-500">{bottomLabel}</small>
{:else}
<small class="text-xs text-slate-500">{bottomLabel}</small>
{/if}
{/if}
</div>

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import clsx from "clsx";
export let cls = "";
</script>
<div class={clsx("w-full border-b", cls)} />

View File

@@ -0,0 +1,56 @@
<script lang="ts">
import clsx from "clsx";
export let disabled: boolean = false;
export let iconleft: any = undefined;
export let iconright: any = undefined;
export let text: string = "";
export let weight: "thin" | "normal" | "medium" | "semibold" | "bold" =
"normal";
export let link: string = "#";
export let onClick: () => void = () => {};
const iconSizing = "h-5 w-5 lg:h-6 lg:w-6";
const baseLinkStyle = clsx(
"gap flex items-center cursor-pointer outline-none focus:outline focus:outline-sky-500 transition-all duration-200 ease-in-out"
);
</script>
{#if disabled}
<div class={baseLinkStyle}>
{#if iconleft}
<svelte:component
this={iconleft}
size={24}
class={clsx(iconSizing, "text-slate-400")}
/>
{/if}
<span class={clsx("text-slate-400", `font-${weight}`)}>
{text}
</span>
{#if iconright}
<svelte:component
this={iconright}
size={24}
class={clsx(iconSizing, "text-slate-400")}
/>
{/if}
</div>
);
{:else}
<a class={baseLinkStyle} on:click={onClick} href={link}>
{#if iconleft}
<svelte:component
this={iconleft}
size={24}
class={clsx(iconSizing, "text-sky-500")}
/>
{/if}
<span class={clsx("text-sky-500", `font-${weight}`)}>
{text}
</span>
{#if iconright}
<svelte:component this={iconright} size={24} class={clsx(iconSizing)} />
{/if}
</a>
{/if}

View File

@@ -0,0 +1,89 @@
<script lang="ts">
import { createDialog } from "@melt-ui/svelte";
import clsx from "clsx";
import IconX from "~icons/bi/x";
const { trigger, portal, overlay, content, title, description, close, open } =
createDialog();
</script>
<div>
<button
{...$trigger}
use:trigger
class={clsx(
"inline-flex items-center justify-center rounded-md bg-white px-4 py-2 font-medium leading-none text-magnum-700 shadow-lg hover:opacity-75"
)}
>
Open Dialog
</button>
<div use:portal>
{#if $open}
<div {...$overlay} class="fixed inset-0 z-20 bg-black/50" />
<div
class="fixed left-[50%] top-[50%] z-30 max-h-[85vh] w-[90vw] max-w-[450px]
translate-x-[-50%] translate-y-[-50%] rounded-md bg-white p-[25px]
shadow-lg"
{...$content}
use:content
>
<h2 {...title} class="m-0 text-lg font-medium text-black">
Edit profile
</h2>
<p {...description} class="mb-5 mt-[10px] leading-normal text-zinc-600">
Make changes to your profile here. Click save when you're done.
</p>
<fieldset class="mb-4 flex items-center gap-5">
<label class="w-[90px] text-right text-magnum-800" for="name">
Name
</label>
<input
class="inline-flex h-8 w-full flex-1 items-center justify-center rounded-sm border
border-solid px-3 leading-none text-magnum-800"
id="name"
value="Thomas G. Lopes"
/>
</fieldset>
<fieldset class="mb-4 flex items-center gap-5">
<label class="w-[90px] text-right text-magnum-800" for="username">
Username
</label>
<input
class="inline-flex h-8 w-full flex-1 items-center justify-center rounded-sm border
border-solid px-3 leading-none text-magnum-800"
id="username"
value="@thomasglopes"
/>
</fieldset>
<div class="mt-[25px] flex justify-end gap-4">
<button
{...close}
use:close
class="inline-flex h-[35px] items-center justify-center rounded-[4px] bg-zinc-100
px-4 font-medium leading-none text-zinc-600"
>
Cancel
</button>
<button
{...close}
use:close
class="inline-flex h-[35px] items-center justify-center rounded-[4px] bg-magnum-100
px-4 font-medium leading-none text-magnum-900"
>
Save changes
</button>
</div>
<button
{...close}
use:close
class="absolute right-[10px] top-[10px] inline-flex h-[25px] w-[25px] appearance-none items-center justify-center rounded-full
text-magnum-800 hover:bg-magnum-100 focus:shadow-magnum-400"
>
<IconX />
</button>
</div>
{/if}
</div>
</div>

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import clsx from "clsx";
export let links: {
href: string;
label: string;
icon: any;
}[];
export let functionalLinks: {
label: string;
icon: any;
onClick: () => void;
}[];
export let linkStyling: string;
</script>
{#each links as link}
<a href={link.href}>
<div class={linkStyling}>
<svelte:component this={link.icon} class={clsx("h-6 w-6")} />
<p>{link.label}</p>
</div>
</a>
{/each}
{#each functionalLinks as link}
<button class={linkStyling} on:click={link.onClick}>
<svelte:component this={link.icon} class={clsx("h-6 w-6")} />
<p>{link.label}</p>
</button>
{/each}

View File

@@ -0,0 +1,82 @@
<script lang="ts">
import { createPagination } from "@melt-ui/svelte";
import IconChevronRight from "~icons/ion/chevron-right";
import IconChevronLeft from "~icons/ion/chevron-left";
import clsx from "clsx";
import Pill from "./pill.svelte";
export let count: number;
export let perPage: number;
export let page: number;
export let siblingCount: number;
export let setPage: (page: number) => void;
export let reducedText: boolean = false;
const { prevButton, nextButton, pages, pageTrigger, range, root } =
createPagination({
count,
perPage,
page,
siblingCount,
});
range.subscribe((r) => {
const newPointer = r.start;
if (newPointer !== page) {
setPage(newPointer / perPage);
}
page = newPointer / perPage;
});
let btnDimensions = "w-10 h-10";
</script>
<nav
class="flex w-full justify-between items-center gap-4 flex-col-reverse md:flex-row"
aria-label="pagination"
{...root}
>
<Pill
theme={"slate"}
smallerText
text={reducedText
? `${count} Entries`
: `Showing ${Number($range.start)} of ${Number($range.end)} entries (${count} rows)`}
/>
<div class="flex items-center gap-2">
<button
class={clsx(
"grid place-items-center rounded-md bg-slate-100 hover:bg-slate-200 text-sky-600 cursor-pointer transition-colors duration-50 active:bg-sky-500 active:text-slate-100",
btnDimensions,
)}
{...$prevButton}
use:prevButton><IconChevronLeft /></button
>
{#each $pages as _page (_page.key)}
{#if _page.type === "ellipsis"}
<span class="font-bold">. . .</span>
{:else}
<button
class={clsx(
btnDimensions,
"grid place-items-center rounded-md cursor-pointer transition-colors duration-50 active:bg-sky-500 active:text-slate-100",
page === _page.value
? "bg-sky-600 text-slate-100 hover:bg-sky-700"
: "bg-slate-100 hover:bg-slate-200 text-sky-600",
)}
{...$pageTrigger(_page)}
use:pageTrigger>{_page.value}</button
>
{/if}
{/each}
<button
class={clsx(
"grid place-items-center rounded-md bg-slate-100 hover:bg-slate-200 text-sky-600 cursor-pointer transition-colors duration-50 active:bg-sky-500 active:text-slate-100",
btnDimensions,
)}
{...$nextButton}
use:nextButton><IconChevronRight /></button
>
</div>
</nav>

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import clsx from "clsx";
const themes = {
green: "bg-green-100 text-green-500",
rose: "bg-rose-100 text-rose-500",
sky: "bg-sky-100 text-sky-500",
darkSky: "bg-sky-200/80 text-sky-800",
slate: "bg-slate-100 text-slate-500",
};
export let text: string = "";
export let smallerText: boolean = false;
export let theme: keyof typeof themes = "green";
</script>
<div
class={clsx(
themes[theme],
"p-2 px-4 font-semibold tracking-wide rounded-full capitalize",
smallerText ? "text-xs" : "text-sm",
)}
>
{text}
</div>

View File

@@ -0,0 +1,39 @@
<script lang="ts">
import clsx from "clsx";
import { randomString } from "$lib/utils";
type Option = {
id?: string | number;
label: string;
value: string;
};
export let componentId: string = randomString(10);
export let label: string = "";
export let options: Option[];
export let defaultChosen: string = "";
export let onSelect: (e: Event) => void = (_) => {};
export let fullWidth: boolean = false;
export let monoFont: boolean = false;
</script>
<div class={clsx("relative flex flex-col gap-1", fullWidth && "w-full")}>
<label class="mb-1 pl-1 text-sm" for={componentId}>{label}</label>
<select
id={componentId}
on:change={onSelect}
class={clsx(
"rounded-md relative cursor-pointer border text-left border-gray-300 outline-none p-2 transition-colors duration-200 bg-slate-50 hover:bg-slate-50 hover:border-black focus:border-sky-500 focus:bg-sky-50 focus:placeholder:text-sky-400 focus:text-sky-700",
fullWidth ? "w-full" : "w-max",
monoFont ? "font-mono" : "",
)}
>
{#each options as option}
<option
selected={defaultChosen === option.value}
class={"bg-white text-black"}
value={option.value}>{option.label}</option
>
{/each}
</select>
</div>

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import clsx from "clsx";
export let roundness: "none" | "md" | "lg" | "xl" | "2xl" | "full" = "lg";
export let height:
| "1"
| "2"
| "3"
| "4"
| "5"
| "6"
| "8"
| "12"
| "16"
| "20"
| "full" = "full";
</script>
<div
class={clsx(
`rounded-${roundness} h-${height} w-full animate-pulse bg-slate-200 p-4 text-slate-200 cursor-wait`,
)}
>
loader
</div>

View File

@@ -0,0 +1,65 @@
<script lang="ts">
import { randomString } from "$lib/utils";
import { createSwitch } from "@melt-ui/svelte";
import clsx from "clsx";
export let label: string;
export let name: string = "";
export let checked: boolean = false;
export let labelColor: "white" | "primary" | "black" = "black";
export let disabled: boolean = false;
export let onChange: (v: boolean) => void = () => {};
export let componentId: string = "";
export let otherOptions: any = {};
$: componentId = componentId || randomString(12);
const {
root,
input,
checked: checkedStore,
isChecked,
options,
} = createSwitch({
name,
disabled,
checked,
});
$: checkedStore.set(checked);
checkedStore.subscribe((v) => {
checked = v;
onChange(v);
});
$: options.update((o) => ({ ...o, disabled }));
</script>
<form>
<div class="flex w-full items-center gap-2">
<button
{...$root}
use:root
id={componentId}
class={clsx(
"relative h-7 min-w-[56px] max-w-[56px] cursor-pointer rounded-full bg-gray-300 outline-none transition-colors data-[state=checked]:bg-sky-500"
)}
{...otherOptions}
>
<span
class={clsx(
"block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[30px]",
$isChecked && "translate-x-[30px]"
)}
/>
<input {...$input} />
</button>
<span
class={clsx(
"md:text-md w-full text-sm font-medium tracking-wide",
labelColor === "white" && "text-white",
(labelColor === undefined || labelColor === "black") && "text-black",
labelColor === "primary" && "text-sky-500"
)}
>
{label}
</span>
</div>
</form>

View File

@@ -0,0 +1,59 @@
<script lang="ts">
import clsx from "clsx";
type TitleSize = "h1" | "h2" | "h3" | "h4";
type TitleFontWeight = "bold" | "semibold" | "normal";
export let text: string = "";
export let size: TitleSize = "h2";
export let weight: TitleFontWeight = "semibold";
export let capitalize: boolean = true;
const weights = {
bold: "font-bold",
semibold: "font-semibold",
normal: "font-normal",
} as const;
</script>
{#if size == "h1"}
<h1
class={clsx(
"text-3xl text-black lg:text-4xl",
weights[weight ?? "bold"],
capitalize && "capitalize",
)}
>
{text}
</h1>
{:else if size == "h2"}
<h2
class={clsx(
"text-2xl text-black lg:text-3xl",
weights[weight ?? "bold"],
capitalize && "capitalize",
)}
>
{text}
</h2>
{:else if size == "h3"}
<h3
class={clsx(
"text-xl text-black lg:text-2xl",
weights[weight ?? "bold"],
capitalize && "capitalize",
)}
>
{text}
</h3>
{:else if size == "h4"}
<h4
class={clsx(
"text-lg text-black lg:text-xl",
weights[weight ?? "bold"],
capitalize && "capitalize",
)}
>
{text}
</h4>
{/if}

View File

@@ -0,0 +1,8 @@
<script lang="ts">
import clsx from "clsx";
import SpinnerRingResize from "~icons/svg-spinners/ring-resize";
</script>
<span class="w-full grid place-items-center py-24">
<SpinnerRingResize class={clsx(`h-8 w-8 text-sky-500`)} />
</span>

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import clsx from "clsx";
import SpinnerSync from "~icons/svg-spinners/blocks-wave";
import SpinnerRingResize from "~icons/svg-spinners/ring-resize";
export let loader: "normal" | "sync" = "normal";
export let color: "white" | "black" | "sky" = "white";
export let resizeToScreen: boolean = false;
</script>
<section
class={clsx(
"grid place-items-center",
resizeToScreen ? "h-screen w-screen" : "h-full w-full",
)}
>
<div class="h-max w-max">
{#if loader === "normal"}
<SpinnerRingResize
class={clsx(
"h-8 w-8",
color === "sky" ? "text-sky-500" : `text-${color}`,
)}
/>
{/if}
{#if loader === "sync"}
<SpinnerSync class={clsx("h-8 w-8", `text-${color}`)} />
{/if}
</div>
</section>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { onDestroy } from "svelte";
let interval: NodeJS.Timeout;
onMount(() => {
interval = setInterval(async () => {
const res = await fetch("/api/auth/verify");
const rC = res.status;
const currentPath = window.location.pathname;
// if rC is not 200 and the current path is not /auth/signin
if (rC !== 200 && !currentPath.includes("/auth/signin")) {
window.location.replace("/auth/signin");
}
}, 1000 * 10);
});
onDestroy(() => {
clearInterval(interval);
});
</script>
<slot />

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
type $$Props = AlertDialogPrimitive.ActionProps;
type $$Events = AlertDialogPrimitive.ActionEvents;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<AlertDialogPrimitive.Action
class={cn(buttonVariants(), className)}
{...$$restProps}
on:click
on:keydown
let:builder
>
<slot {builder} />
</AlertDialogPrimitive.Action>

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
type $$Props = AlertDialogPrimitive.CancelProps;
type $$Events = AlertDialogPrimitive.CancelEvents;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<AlertDialogPrimitive.Cancel
class={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
{...$$restProps}
on:click
on:keydown
let:builder
>
<slot {builder} />
</AlertDialogPrimitive.Cancel>

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import * as AlertDialog from "./index.js";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = AlertDialogPrimitive.ContentProps;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<AlertDialog.Portal>
<AlertDialog.Overlay />
<AlertDialogPrimitive.Content
{transition}
{transitionConfig}
class={cn(
"bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg md:w-full",
className
)}
{...$$restProps}
>
<slot />
</AlertDialogPrimitive.Content>
</AlertDialog.Portal>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = AlertDialogPrimitive.DescriptionProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<AlertDialogPrimitive.Description
class={cn("text-muted-foreground text-sm", className)}
{...$$restProps}
>
<slot />
</AlertDialogPrimitive.Description>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...$$restProps}
>
<slot />
</div>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...$$restProps}>
<slot />
</div>

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { fade } from "svelte/transition";
import { cn } from "$lib/utils.js";
type $$Props = AlertDialogPrimitive.OverlayProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = fade;
export let transitionConfig: $$Props["transitionConfig"] = {
duration: 150,
};
export { className as class };
</script>
<AlertDialogPrimitive.Overlay
{transition}
{transitionConfig}
class={cn("bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ", className)}
{...$$restProps}
/>

View File

@@ -0,0 +1,9 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
type $$Props = AlertDialogPrimitive.PortalProps;
</script>
<AlertDialogPrimitive.Portal {...$$restProps}>
<slot />
</AlertDialogPrimitive.Portal>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = AlertDialogPrimitive.TitleProps;
let className: $$Props["class"] = undefined;
export let level: $$Props["level"] = "h3";
export { className as class };
</script>
<AlertDialogPrimitive.Title class={cn("text-lg font-semibold", className)} {level} {...$$restProps}>
<slot />
</AlertDialogPrimitive.Title>

View File

@@ -0,0 +1,40 @@
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import Title from "./alert-dialog-title.svelte";
import Action from "./alert-dialog-action.svelte";
import Cancel from "./alert-dialog-cancel.svelte";
import Portal from "./alert-dialog-portal.svelte";
import Footer from "./alert-dialog-footer.svelte";
import Header from "./alert-dialog-header.svelte";
import Overlay from "./alert-dialog-overlay.svelte";
import Content from "./alert-dialog-content.svelte";
import Description from "./alert-dialog-description.svelte";
const Root = AlertDialogPrimitive.Root;
const Trigger = AlertDialogPrimitive.Trigger;
export {
Root,
Title,
Action,
Cancel,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
//
Root as AlertDialog,
Title as AlertDialogTitle,
Action as AlertDialogAction,
Cancel as AlertDialogCancel,
Portal as AlertDialogPortal,
Footer as AlertDialogFooter,
Header as AlertDialogHeader,
Trigger as AlertDialogTrigger,
Overlay as AlertDialogOverlay,
Content as AlertDialogContent,
Description as AlertDialogDescription,
};

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import { Button as ButtonPrimitive } from "bits-ui";
import { type Events, type Props, buttonVariants } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = Props;
type $$Events = Events;
let className: $$Props["class"] = undefined;
export let variant: $$Props["variant"] = "default";
export let size: $$Props["size"] = "default";
export let builders: $$Props["builders"] = [];
export { className as class };
</script>
<ButtonPrimitive.Root
{builders}
class={cn(buttonVariants({ variant, size, className }))}
type="button"
{...$$restProps}
on:click
on:keydown
>
<slot />
</ButtonPrimitive.Root>

View File

@@ -0,0 +1,54 @@
import { type VariantProps, tv } from "tailwind-variants";
import type { Button as ButtonPrimitive } from "bits-ui";
import Root from "./button.svelte";
const buttonVariants = tv({
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
variants: {
variant: {
default: "bg-sky-500 text-white hover:bg-sky-600/90",
primaryInverted: "bg-sky-50 text-sky-600 hover:bg-sky-100",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
destructiveInverted:
"bg-destructive/10 text-rose-500 hover:bg-destructive/30",
success: "bg-emerald-500 text-emerald-50 hover:bg-emerald-600/90",
outline:
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
type Variant = VariantProps<typeof buttonVariants>["variant"];
type Size = VariantProps<typeof buttonVariants>["size"];
type Props = ButtonPrimitive.Props & {
variant?: Variant;
size?: Size;
};
type Events = ButtonPrimitive.Events;
export {
Root,
type Props,
type Events,
//
Root as Button,
type Props as ButtonProps,
type Events as ButtonEvents,
buttonVariants,
};

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import { Checkbox as CheckboxPrimitive } from "bits-ui";
import Check from "~icons/mdi/check";
import Minus from "~icons/ic/outline-minus";
import { cn } from "$lib/utils.js";
type $$Props = CheckboxPrimitive.Props;
type $$Events = CheckboxPrimitive.Events;
let className: $$Props["class"] = undefined;
export let checked: $$Props["checked"] = false;
export { className as class };
</script>
<CheckboxPrimitive.Root
class={cn(
"border-sky-900 ring-offset-background focus-visible:ring-ring data-[state=checked]:border-sky-600 data-[state=checked]:bg-sky-600 data-[state=checked]:text-primary-foreground peer box-content h-4 w-4 shrink-0 rounded-sm border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50",
className,
)}
bind:checked
{...$$restProps}
on:click
>
<CheckboxPrimitive.Indicator
class={cn("flex h-4 w-4 items-center justify-center text-current")}
let:isChecked
let:isIndeterminate
>
{#if isChecked}
<Check class="h-3.5 w-3.5" />
{:else if isIndeterminate}
<Minus class="h-3.5 w-3.5" />
{/if}
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>

View File

@@ -0,0 +1,6 @@
import Root from "./checkbox.svelte";
export {
Root,
//
Root as Checkbox,
};

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import X from "~icons/mdi/window-close";
import * as Dialog from "./index.js";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = DialogPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = {
duration: 200,
};
export { className as class };
</script>
<Dialog.Portal>
<Dialog.Overlay />
<DialogPrimitive.Content
{transition}
{transitionConfig}
class={cn(
"bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg md:w-full",
className,
)}
{...$$restProps}
>
<slot />
<DialogPrimitive.Close
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
>
<X class="h-4 w-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</Dialog.Portal>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DialogPrimitive.DescriptionProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DialogPrimitive.Description
class={cn("text-muted-foreground text-sm", className)}
{...$$restProps}
>
<slot />
</DialogPrimitive.Description>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...$$restProps}
>
<slot />
</div>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...$$restProps}>
<slot />
</div>

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { fade } from "svelte/transition";
import { cn } from "$lib/utils.js";
type $$Props = DialogPrimitive.OverlayProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = fade;
export let transitionConfig: $$Props["transitionConfig"] = {
duration: 150,
};
export { className as class };
</script>
<DialogPrimitive.Overlay
{transition}
{transitionConfig}
class={cn("bg-background/80 fixed inset-0 z-50 backdrop-blur-sm", className)}
{...$$restProps}
/>

View File

@@ -0,0 +1,8 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
type $$Props = DialogPrimitive.PortalProps;
</script>
<DialogPrimitive.Portal {...$$restProps}>
<slot />
</DialogPrimitive.Portal>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DialogPrimitive.TitleProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DialogPrimitive.Title
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...$$restProps}
>
<slot />
</DialogPrimitive.Title>

View File

@@ -0,0 +1,37 @@
import { Dialog as DialogPrimitive } from "bits-ui";
import Title from "./dialog-title.svelte";
import Portal from "./dialog-portal.svelte";
import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte";
import Overlay from "./dialog-overlay.svelte";
import Content from "./dialog-content.svelte";
import Description from "./dialog-description.svelte";
const Root = DialogPrimitive.Root;
const Trigger = DialogPrimitive.Trigger;
const Close = DialogPrimitive.Close;
export {
Root,
Title,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Close,
//
Root as Dialog,
Title as DialogTitle,
Portal as DialogPortal,
Footer as DialogFooter,
Header as DialogHeader,
Trigger as DialogTrigger,
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription,
Close as DialogClose,
};

View File

@@ -0,0 +1,29 @@
import Root from "./input.svelte";
export type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
};
export type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
mousemove: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
wheel: FormInputEvent<WheelEvent>;
};
export {
Root,
//
Root as Input,
};

View File

@@ -0,0 +1,42 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import type { InputEvents } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined;
</script>
<input
class={cn(
"border-gray-300 placeholder:text-muted-foreground flex w-full rounded-md border px-3 py-2 focus-visible:outline-none duration-200 transition-colors disabled:cursor-not-allowed disabled:opacity-50 bg-slate-50 hover:bg-slate-50 hover:border-black focus:border-sky-500 focus:bg-sky-50 focus:placeholder:text-sky-400 focus:text-sky-700",
className,
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:mousemove
on:paste
on:input
on:wheel|passive
{...$$restProps}
/>

View File

@@ -0,0 +1,7 @@
import Root from "./label.svelte";
export {
Root,
//
Root as Label,
};

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = LabelPrimitive.Props;
type $$Events = LabelPrimitive.Events;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<LabelPrimitive.Root
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...$$restProps}
on:mousedown
>
<slot />
</LabelPrimitive.Root>

View File

@@ -0,0 +1,28 @@
import Root from "./table.svelte";
import Body from "./table-body.svelte";
import Caption from "./table-caption.svelte";
import Cell from "./table-cell.svelte";
import Footer from "./table-footer.svelte";
import Head from "./table-head.svelte";
import Header from "./table-header.svelte";
import Row from "./table-row.svelte";
export {
Root,
Body,
Caption,
Cell,
Footer,
Head,
Header,
Row,
//
Root as Table,
Body as TableBody,
Caption as TableCaption,
Cell as TableCell,
Footer as TableFooter,
Head as TableHead,
Header as TableHeader,
Row as TableRow,
};

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLTableSectionElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<tbody class={cn("[&_tr:last-child]:border-0", className)} {...$$restProps}>
<slot />
</tbody>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLTableCaptionElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<caption class={cn("text-muted-foreground mt-4 text-sm", className)} {...$$restProps}>
<slot />
</caption>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import type { HTMLTdAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLTdAttributes;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<td
class={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...$$restProps}
on:click
on:keydown
>
<slot />
</td>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLTableSectionElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<tfoot class={cn("bg-primary text-primary-foreground font-medium", className)} {...$$restProps}>
<slot />
</tfoot>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import type { HTMLThAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLThAttributes;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<th
class={cn(
"text-muted-foreground h-12 px-4 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0",
className
)}
{...$$restProps}
>
<slot />
</th>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLTableSectionElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<thead class={cn("[&_tr]:border-b", className)} {...$$restProps} on:click on:keydown>
<slot />
</thead>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLTableRowElement> & {
"data-state"?: unknown;
};
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<tr
class={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...$$restProps}
on:click
on:keydown
>
<slot />
</tr>

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLTableAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLTableAttributes;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class="relative w-full overflow-auto">
<table class={cn("w-full caption-bottom text-sm", className)} {...$$restProps}>
<slot />
</table>
</div>

View File

@@ -0,0 +1,28 @@
import Root from "./textarea.svelte";
type FormTextareaEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLTextAreaElement;
};
type TextareaEvents = {
blur: FormTextareaEvent<FocusEvent>;
change: FormTextareaEvent<Event>;
click: FormTextareaEvent<MouseEvent>;
focus: FormTextareaEvent<FocusEvent>;
keydown: FormTextareaEvent<KeyboardEvent>;
keypress: FormTextareaEvent<KeyboardEvent>;
keyup: FormTextareaEvent<KeyboardEvent>;
mouseover: FormTextareaEvent<MouseEvent>;
mouseenter: FormTextareaEvent<MouseEvent>;
mouseleave: FormTextareaEvent<MouseEvent>;
paste: FormTextareaEvent<ClipboardEvent>;
input: FormTextareaEvent<InputEvent>;
};
export {
Root,
//
Root as Textarea,
type TextareaEvents,
type FormTextareaEvent,
};

View File

@@ -0,0 +1,38 @@
<script lang="ts">
import type { HTMLTextareaAttributes } from "svelte/elements";
import type { TextareaEvents } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = HTMLTextareaAttributes;
type $$Events = TextareaEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined;
</script>
<textarea
class={cn(
"border-gray-300 placeholder:text-muted-foreground flex min-h-24 w-full rounded-md border px-3 py-2 focus-visible:outline-none duration-200 transition-colors disabled:cursor-not-allowed disabled:opacity-50 bg-slate-50 hover:bg-slate-50 hover:border-black focus:border-sky-500 focus:bg-sky-50 focus:placeholder:text-sky-400 focus:text-sky-700",
className,
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
></textarea>

9
src/lib/server/array.chunk.ts Executable file
View File

@@ -0,0 +1,9 @@
export function chunkArray<T>(array: T[], size: number): T[][] {
const chunked_arr = [];
let index = 0;
while (index < array.length) {
chunked_arr.push(array.slice(index, size + index));
index += size;
}
return chunked_arr;
}

View File

@@ -0,0 +1,10 @@
import Redis from "ioredis";
const redisUrl = process.env.REDIS_URL ?? "";
console.log(`\n\n[//] Redis URL: ${redisUrl}`);
const _redis =
redisUrl && redisUrl.length > 0 ? new Redis(redisUrl) : undefined;
export const redis = _redis as Redis;

View File

@@ -0,0 +1,36 @@
import Surreal from "surrealdb.js";
export type { QueryResult } from "surrealdb.js/script/types";
try {
if (document || window) {
throw new Error("SurrealDB needs a NodeJS environment to run.");
}
} catch (err) {}
const CONFIG = {
url: process.env.SURREAL_URL ?? "",
user: process.env.SURREAL_USER ?? "",
pass: process.env.SURREAL_PASS ?? "",
ns: process.env.SURREAL_NS ?? "",
db: process.env.SURREAL_DB ?? "",
} as const;
// for (let key in CONFIG) {
// if (
// !CONFIG[key as keyof typeof CONFIG] ||
// CONFIG[key as keyof typeof CONFIG] === ""
// ) {
// throw new Error(`Missing configuration for ${key}`);
// }
// }
let _surreal =
CONFIG.url.length > 0
? new Surreal(`http://${CONFIG.url}/rpc`, {
auth: { user: CONFIG.user, pass: CONFIG.pass },
ns: CONFIG.ns,
db: CONFIG.db,
})
: undefined;
export const surreal = _surreal as Surreal;

View File

@@ -0,0 +1,50 @@
import { redis } from "$lib/server/connectors/redis";
export type SessionData = {
username: string;
userType: string;
};
export const parseCookieString = (cookieString: string) => {
const cookies: Record<string, string> = cookieString
.split(";")
.reduce((acc, cookie) => {
const [key, value] = cookie.split("=");
if (!key || !value) {
return acc;
}
return { ...acc, [key.trim()]: decodeURIComponent(value) };
}, {});
for (const key in cookies) {
if (!key.length || !cookies[key] || cookies[key] === "undefined") {
delete cookies[key];
}
}
return cookies;
};
export const parseSessionData = (sessionData: string) => {
const splits = sessionData.split("|");
if (splits.length < 5) return false;
return { username: splits[2], userType: splits[3] } as SessionData;
};
export const getParsedSession = async (sId: string) => {
const session = await redis.get(sId);
if (!session) {
return false;
}
const parsed = parseSessionData(session);
if (!parsed) {
return false;
}
return parsed;
};
export const isSessionValid = async (sId: string) => {
if (!sId || sId.length === 0 || sId.length < 20) {
return false;
}
const session = await redis.get(sId);
return session && session.length > 1;
};

212
src/lib/server/db/apidata.db.ts Executable file
View File

@@ -0,0 +1,212 @@
import type { BookingEntry } from "$lib/utils/data.types";
import { chunkArray } from "../array.chunk";
import { surreal } from "../connectors/surreal.db";
const getTableName = (date: string) => {
return `apidata${date.replaceAll("-", "")}`;
};
const upsertData = async (
data: BookingEntry[],
date: string,
tries: number = 0,
): Promise<void> => {
const tableName = getTableName(date);
console.log(`[...] Upserting ${data.length} entries into ${tableName}`);
const alreadyPresentIds = new Set();
try {
const [alreadyPresent] = await surreal.query<[string[]]>(
`select value id from type::table($tableName) where bookDate = $bookDate`,
{ tableName, bookDate: date },
);
for (let eId of alreadyPresent.result ?? []) {
alreadyPresentIds.add(eId);
}
} catch (err) {
console.log("Failed to fetch, seeing if can try again");
if (tries >= 3) {
console.log("Max tries exceeded for initial fetch for upserting data");
return;
}
return await upsertData(data, date, tries++);
}
const oldEntries = [] as any[];
const newEntries = [] as BookingEntry[];
for (let entry of data) {
if (!alreadyPresentIds.has(entry.id)) {
newEntries.push({
...entry,
id: `${tableName}:${entry.id}`,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
bookDate: entry.bookDate.split(" ")[0],
requestId: entry.requestId ?? "",
});
} else {
oldEntries.push({
distributorId: entry.distributorId,
dealerId: entry.dealerId,
drawId: entry.drawId,
bookDate: entry.bookDate.split(" ")[0],
number: entry.number,
first: entry.first,
second: entry.second,
changedBalance: entry.changedBalance,
sheetName: entry.sheetName,
sheetId: entry.sheetId,
requestId: entry.requestId,
updatedAt: new Date().toISOString(),
});
}
}
console.log(
`[+] Inserting ${newEntries.length} new entries into ${tableName}`,
);
// 5 to 25% of the total data length
let chunkSize = Math.floor(
Math.random() * (data.length * 0.25 - data.length * 0.05) +
data.length * 0.05,
);
if (chunkSize > 10_000) {
chunkSize = 10_000;
}
console.log(`Chunk Size : ${chunkSize}`);
console.log(`[+] Inserting new entries`);
console.time("insertion time");
const chunks = chunkArray(newEntries, chunkSize).map(async (chunk) => {
await surreal.insert<BookingEntry>(tableName, chunk);
});
for (let i = 0; i < chunks.length; i += 2) {
await Promise.all(chunks.slice(i, i + 2));
}
console.timeEnd("insertion time");
console.log(
`[+] Updating ${oldEntries.length} old entries into ${tableName}`,
);
const chunks2 = chunkArray(oldEntries, chunkSize).map(async (chunk) => {
await Promise.all(
chunk.map(async (entry) => {
// @ts-ignore
await surreal.update<BookingEntry>(`${tableName}:${entry.id}`, {
distributorId: entry.distributorId,
dealerId: entry.dealerId,
drawId: entry.drawId,
bookDate: entry.bookDate.split(" ")[0],
number: entry.number,
first: entry.first,
second: entry.second,
changedBalance: entry.changedBalance,
sheetName: entry.sheetName,
sheetId: entry.sheetId,
requestId: entry.requestId,
updatedAt: new Date().toISOString(),
});
}),
);
});
console.time("update time");
for (let i = 0; i < chunks2.length; i += 10) {
await Promise.all(chunks2.slice(i, i + 10));
}
console.timeEnd("update time");
console.log(
`[+] Successfully upserted ${data.length} entries into ${tableName}`,
);
};
const getBookingEntriesForDealer = async (
date: string,
drawId: string,
userId: string,
sorted?: boolean,
) => {
const tableName = getTableName(date);
let query = `select * from type::table($tableName) where bookDate = $date and dealerId = $userId and drawId = $drawId`;
if (sorted) {
query += " order by requestId desc";
}
const [data] = await surreal.query<[BookingEntry[]]>(query, {
tableName,
date: `${date}`,
userId: parseInt(userId),
drawId: parseInt(drawId),
});
console.log(
`Found ${JSON.stringify(
data,
)} entries for ${userId}, filters are ${date}, ${drawId} for ${tableName}`,
);
if (data.status === "OK") {
return data.result ?? [];
}
return [];
};
const getBookingEntriesByDraw = async (date: string, drawId: string) => {
const tableName = getTableName(date);
const [data] = await surreal.query<[BookingEntry[]]>(
`select * from type::table($tableName) where bookDate = $date and drawId = $drawId`,
{
tableName,
date: date,
drawId: parseInt(drawId.includes(":") ? drawId.split(":")[1] : drawId),
},
);
if (data.status === "OK") {
return data.result ?? [];
}
return [];
};
const deleteDataOlderThan2Weeks = async () => {
const [out] = await surreal.query("info for db");
// @ts-ignore
const tableNames = Object.keys(out.result.tables);
const twoWeeksAgo = new Date();
twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14);
for (const tableName of tableNames) {
if (tableName.startsWith("apidata")) {
const datePart = tableName.slice(7);
const d = new Date(
parseInt(datePart.slice(0, 4), 10),
parseInt(datePart.slice(4, 6), 10) - 1, // Month is 0-based in JavaScript Date
parseInt(datePart.slice(6, 8), 10),
);
if (d < twoWeeksAgo) {
console.log(`[...] Deleting ${tableName}`);
await surreal.query("remove table if exists " + tableName);
console.log(`[+] Deleted ${tableName}`);
}
} else if (tableName.startsWith("apipostdata_")) {
const datePart = tableName.slice(12);
const d = new Date(
parseInt(datePart.slice(0, 4), 10),
parseInt(datePart.slice(4, 6), 10) - 1, // Month is 0-based in JavaScript Date
parseInt(datePart.slice(6, 8), 10),
);
if (d < twoWeeksAgo) {
console.log(`[...] Deleting ${tableName}`);
await surreal.query("remove table if exists " + tableName);
console.log(`[+] Deleted ${tableName}`);
}
} else {
console.log(`Skipping ${tableName}`);
}
}
};
export const dbApiData = {
upsertData,
getBookingEntriesForDealer,
getBookingEntriesByDraw,
deleteDataOlderThan2Weeks,
};

100
src/lib/server/db/apidraw.db.ts Executable file
View File

@@ -0,0 +1,100 @@
import { constants } from "$lib/utils/constants";
import type { Draw } from "$lib/utils/data.types";
import { getDraws } from "../external/api.scraping.helpers";
import { surreal } from "../connectors/surreal.db";
import { getSessionFromStore } from "../utils/session.service";
const tableName = "apidraw";
const _populateDrawsTable = async () => {
const session = await getSessionFromStore(constants.SCRAP_API_SESSION_KEY);
if (!session) {
return;
}
const draws = await getDraws(session?.sessionToken);
if (draws.data.length === 0 || !draws.ok) {
return;
}
await surreal.insert<Draw>(
tableName,
draws.data.map((e) => {
return {
id: e.id,
drawType: e.drawType,
adminId: e.adminId,
title: e.title,
closeTime: e.closeTime,
filterDuplicatesWhilePosting: false,
abRateF: 0,
abcRateF: 0,
abRateS: 0,
abcRateS: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
}),
);
};
const getAllDraws = async (
skipOptional?: boolean,
retry: number = 0,
): Promise<Draw[]> => {
let query = `select * from apidraw order by closeTime`;
const [out] = await surreal.query<[Draw[]]>(query);
if (out.status === "OK") {
const draws = out.result ?? [];
if (draws.length > 0) {
return draws;
}
await _populateDrawsTable();
if (retry < 3) {
return getAllDraws(skipOptional, retry + 1);
}
}
return [];
};
async function setFilterDuplicatesFlag(drawId: string, flag: boolean) {
const [d] = await surreal.select<Draw>(drawId);
if (!d || !d.id) {
return;
}
console.log("setFilterDuplicatesFlag :: ", drawId, flag);
await surreal.update(drawId, {
...d,
filterDuplicatesWhilePosting: flag,
updatedAt: new Date().toISOString(),
} as Draw);
}
async function updateDrawPresetInfo(draw: Draw) {
const drawId = draw.id;
const [d] = await surreal.select<Draw>(drawId);
if (!d || !d.id) {
return;
}
await surreal.update(drawId, {
...d,
filterDuplicatesWhilePosting: draw.filterDuplicatesWhilePosting,
abRateF: draw.abRateF,
abcRateF: draw.abcRateF,
abRateS: draw.abRateS,
abcRateS: draw.abcRateS,
updatedAt: new Date().toISOString(),
} as Draw);
}
const getDraw = async (drawId: string): Promise<Draw | undefined> => {
const draws = await surreal.select<Draw>(
drawId.includes("apidraw") ? drawId : `apidraw:${drawId}`,
);
return draws[0];
};
export const dbDraw = {
getAllDraws,
getDraw,
setFilterDuplicatesFlag,
updateDrawPresetInfo,
};

View File

@@ -0,0 +1,96 @@
import type { PostDataEntry, PostDataHistory } from "$lib/utils/data.types";
import { surreal } from "../connectors/surreal.db";
const getTableName = (date: string) => {
return `apipostdata_${date.replaceAll("-", "")}`;
};
const upsertData = async (data: PostDataHistory) => {
const tableName = getTableName(data.bookDate);
const [check] = await surreal.query<[PostDataHistory[]]>(
`select * from type::table($tableName) where bookDate = $bookDate and drawId = $drawId`,
{ tableName, bookDate: data.bookDate, drawId: data.drawId },
);
console.log(check);
const firstOut = check.result ? check.result[0] : undefined;
if (check.status === "OK" && !!firstOut && !!firstOut.id) {
console.log(
`Adding ${data.data.length} entries to ${firstOut.data.length} existing array`,
);
firstOut.data.push(...data.data);
console.log(`Now have ${firstOut.data.length} entries in data list`);
console.log(`[...] Updating data row in db now`);
await surreal.update<PostDataHistory>(firstOut.id, {
id: firstOut.id,
data: firstOut.data,
drawId: firstOut.drawId,
bookDate: firstOut.bookDate,
updatedAt: new Date().toISOString(),
});
return;
}
await surreal.insert<PostDataHistory>(tableName, data);
console.log(
`[+] Inserted post data in ${tableName} for ${data.bookDate} - ${data.drawId}`,
);
};
const getPostDataByDraw = async (date: string, drawId: string) => {
const tableName = getTableName(date);
const [data] = await surreal.query<[PostDataHistory[]]>(
`select * from type::table($tableName) where bookDate = $date and drawId = $drawId`,
{
tableName,
date: date,
drawId: parseInt(drawId.includes(":") ? drawId.split(":")[1] : drawId),
},
);
let out = [] as PostDataEntry[];
if (data.status === "OK" && data.result.length > 0) {
out = data.result[0].data;
}
return out;
};
async function doesPostHistoryDataExist(date: string, drawId: string) {
const tableName = getTableName(date);
const [data] = await surreal.query<[PostDataHistory[]]>(
`select id from type::table($tableName) where bookDate = $date and drawId = $drawId`,
{
tableName,
date: date,
drawId: parseInt(drawId.includes(":") ? drawId.split(":")[1] : drawId),
},
);
if (data.status === "OK") {
return data.result[0]?.id.length > 0;
}
return false;
}
async function deletePostDataByDraw(date: string, drawId: string) {
const tableName = getTableName(date);
const [data] = await surreal.query<[PostDataHistory[]]>(
`select id from type::table($tableName) where bookDate = $date and drawId = $drawId`,
{
tableName,
date: date,
drawId: parseInt(drawId.includes(":") ? drawId.split(":")[1] : drawId),
},
);
if (data.status === "OK") {
await surreal.delete(tableName);
return true;
}
return false;
}
export const dbApiPostData = {
upsertData,
getPostDataByDraw,
deletePostDataByDraw,
doesPostHistoryDataExist,
};

279
src/lib/server/db/apiuser.db.ts Executable file
View File

@@ -0,0 +1,279 @@
import {
type ApiUser,
type LooseApiUser,
type ApiPostUser,
type ApiPostUserWithParent,
ApiUserTypes,
DEFAULT_RANDOM_DISTRIBUTOR,
} from "$lib/utils/data.types";
import { surreal } from "../connectors/surreal.db";
const getUserById = async (userId: string) => {
const query = `select * from apiuser where id = $id`;
const [rizzult] = await surreal.query<[ApiUser[]]>(query, { id: userId });
return rizzult.result?.[0];
};
const getAllIdsByUserType = async (userType: number) => {
const query = `select value id from apiuser where userType = $userType`;
const rizzult = (await surreal.query<[string[]]>(query, { userType }))[0];
return (rizzult.result ?? []).map((e) => {
return e.split(":")[1];
});
};
async function allUsersOfTypeLimitedInfo(userType: number) {
const rizzult = (
await surreal.query<[ApiPostUser[]]>(
`select id,userName,userId,postData from apiuser where userType = $userType`,
{ userType: userType },
)
)[0];
if (rizzult.status == "OK") {
return rizzult.result ?? [];
}
return [];
}
async function setPostDataFlagForUsers(users: ApiPostUser[]) {
for (const user of users) {
const [u] = await surreal.select<ApiUser>(user.id);
if (!u || !u.id) {
continue;
}
await surreal.update<LooseApiUser>(user.id, {
...u,
postData: user.postData ?? false,
});
}
}
const getUserTypeCount = async (userType: number) => {
const queryBase = `select count() from apiuser where userType = $userType`;
let query = `${queryBase} and disable = 0 group all`;
let disabledQuery = `${queryBase} and disable = 1 group all`;
const enabledRizzult = (
await surreal.query<[{ count: number }[]]>(query, { userType: userType })
)[0];
const count = { enabled: 0, disabled: 0 };
if (enabledRizzult.status == "OK") {
count.enabled = enabledRizzult.result[0]?.count ?? 0;
}
const disabledRizzult = (
await surreal.query<[{ count: number }[]]>(disabledQuery, {
userType: userType,
})
)[0];
if (disabledRizzult.status == "OK") {
count.disabled = disabledRizzult.result[0]?.count ?? 0;
}
return count;
};
const allUsersOfType = async (userType: number) => {
const rizzult = (
await surreal.query<[ApiUser[]]>(
`select * from apiuser where userType = $userType`,
{ userType: userType },
)
)[0];
if (rizzult.status == "OK") {
return rizzult.result ?? [];
}
return [];
};
async function updatePostUsersBalances(
payload: { balance: number; id: string }[],
) {
console.log("Updating users balances");
console.log(payload);
for (const each of payload) {
const [rizzult] = await surreal.query<[ApiUser[]]>(
`update $userId set balance = $balance`,
{ userId: each.id, balance: each.balance },
);
if (rizzult.status !== "OK") {
console.error("updatePostUsersBalance :: ", rizzult);
}
}
console.log("Users balances updated");
}
async function getAllPostUsers() {
const [rizzult] = await surreal.query<[ApiPostUser[]]>(
`select id,userName,userId,postData from apiuser where postData = true`,
);
if (rizzult.status === "OK") {
return rizzult.result ?? [];
}
return [];
}
async function getAllPostUsersWithParentUsers() {
const [rizzult] = await surreal.query<[ApiPostUserWithParent[]]>(
`select id,userName,userId,postData,parentDistributor,parentAdmin from apiuser where postData = true`,
);
if (rizzult.status === "OK") {
return rizzult.result ?? [];
}
return [];
}
const getAllDistributorsWithTheirChildren = async () => {
const distributorIds = await getAllIdsByUserType(ApiUserTypes.DISTRIBUTOR);
const out = distributorIds.map(async (id) => {
const [rizzult] = await surreal.query<[ApiUser[]]>(
`select *, (select * from apiuser where parentDistributor = $id) as children from apiuser where id = $prefixedId`,
{ id, prefixedId: `apiuser:${id}` },
);
if (rizzult.status == "OK") {
return rizzult.result[0];
}
return undefined;
});
const responses = await Promise.all(out);
return responses;
};
const getRandomDistributor = async (): Promise<ApiUser> => {
const ignoreList = ["001OP9"];
const randomUser = await _getRandomUser(ApiUserTypes.DISTRIBUTOR, ignoreList);
if (!randomUser) {
return DEFAULT_RANDOM_DISTRIBUTOR;
}
return randomUser as any as ApiUser;
};
const getRandomDealer = async (): Promise<ApiUser | undefined> => {
const ignoreList = ["rizgnore"];
return _getRandomUser(ApiUserTypes.DEALER, ignoreList);
};
const _getRandomUser = async (
userType: number,
ignoreList: string[],
): Promise<ApiUser | undefined> => {
console.log("_getRandomUser :: ", userType);
const rizzult = (
await surreal.query<[ApiUser[]]>(
`select * from apiuser where disable = 0 and userType = $userType and userId notinside $ignoreList order by rand() limit 1`,
{ userType: userType, ignoreList: ignoreList },
)
)[0];
if (rizzult.status == "OK") {
return rizzult.result[0];
}
};
const doesExist = async (userId?: string) => {
console.log("doesExist :: ", userId);
if (userId) {
const [rizzult] = await surreal.query<{ count: number }[]>(
"select count() from apiuser where userId = $userId group all",
{ userId: userId },
);
if (rizzult.status == "OK") {
return rizzult.result?.count > 0;
}
}
return false;
};
const insertMany = async (data: LooseApiUser[], postUsers: ApiPostUser[]) => {
console.log("insertMany :: ", data.length);
await surreal.insert<LooseApiUser>(
"apiuser",
data.map((e) => {
return {
...e,
postData: !!postUsers.find((u) => u.userId === e.userId),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
}),
);
};
async function upsertMany(
data: LooseApiUser[],
wipeTable: boolean,
deleteUserType: typeof ApiUserTypes.DISTRIBUTOR | typeof ApiUserTypes.DEALER,
) {
const postUsers = await getAllPostUsers();
console.log(postUsers);
if (wipeTable) {
console.log("[wipeTable] :: deleting all previous users");
await surreal.query("delete from apiuser where userType = $userType", {
userType: deleteUserType,
});
}
console.log("upsertMany :: ", data.length);
const toCreate = [] as LooseApiUser[];
const out = data.map(async (apiUser) => {
// INFO: if you do want to keep disabled users, remove this check
if (apiUser.disable === 1) {
return;
}
const [u] = await surreal.select<ApiUser>(`apiuser:${apiUser.id}`);
if (!u || !u.id) {
toCreate.push(apiUser);
return;
}
let postData =
u.postData ?? !!postUsers.find((pu) => pu.userId === u.userId) ?? false;
const qId = u.id;
await surreal.update<LooseApiUser>(qId, {
id: u.id,
userId: apiUser.userId,
userType: apiUser.userType,
disableBooking: apiUser.disableBooking,
sendVoucher: apiUser.sendVoucher,
voucherGenerated: apiUser.voucherGenerated,
parentAdmin: apiUser.parentAdmin,
parentDistributor: apiUser.parentDistributor,
userName: apiUser.userName,
userCity: apiUser.userCity,
password: apiUser.password,
accessDenied: apiUser.accessDenied,
phoneNumber: apiUser.phoneNumber,
emailAddress: apiUser.emailAddress,
disable: apiUser.disable,
commission: apiUser.commission,
commissionPangora: apiUser.commissionPangora,
allowTitles: apiUser.allowTitles,
specialDealer: apiUser.specialDealer,
allowBalance: apiUser.allowBalance,
balance: apiUser.balance,
profitlossShare: apiUser.profitlossShare,
shareProfitonly: apiUser.shareProfitonly,
allowRemoveold: apiUser.allowRemoveold,
removeDays: apiUser.removeDays,
language: apiUser.language,
postData,
createdAt: u.createdAt,
updatedAt: new Date().toISOString(),
});
});
await Promise.allSettled(out);
if (toCreate.length > 0) {
await insertMany(toCreate, postUsers);
}
}
export const dbApiUser = {
allUsersOfType,
allUsersOfTypeLimitedInfo,
getUserById,
getAllDistributorsWithTheirChildren,
getUserTypeCount,
getAllIdsByUserType,
getAllPostUsers,
getAllPostUsersWithParentUsers,
getRandomDistributor,
getRandomDealer,
doesExist,
upsertMany,
setPostDataFlagForUsers,
updatePostUsersBalances,
};

View File

@@ -0,0 +1,4 @@
export const dbBooking = {
};

View File

@@ -0,0 +1,40 @@
import type { FinalSheetData } from "$lib/utils/data.types";
import { surreal } from "../connectors/surreal.db";
const getTableName = (date: string) => {
return `finalsheet${date.replaceAll("-", "")}`;
};
const upsertData = async (data: FinalSheetData, date: string) => {
const tableName = getTableName(date);
const [present] = await surreal.query<[FinalSheetData[]]>(
`select id from type::table($tableName) where date = $date and drawId = $drawId`,
{ tableName, date: `${date}`, drawId: data.drawId }
);
if (present) {
// @ts-ignore
await surreal.update<FinalSheetData>(`${tableName}:${data.id}`, {
date: data.date,
drawId: data.drawId,
data: data.data,
totals: data.totals,
// @ts-ignore
createdAt: present?.result[0]?.createdAt ?? new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
} else {
// @ts-ignore
await surreal.create<FinalSheetData>(`${tableName}:${data.id}`, {
date: data.date,
drawId: data.drawId,
data: data.data,
totals: data.totals,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
}
};
export const dbFinalSheet = {
upsertData,
};

View File

@@ -0,0 +1,65 @@
import type {
ApiPostUser,
PostDataEntry,
PresetDataEntry,
} from "$lib/utils/data.types";
import { surreal } from "../connectors/surreal.db";
const getTableName = (date: string) => {
return `presetdata_${date.replaceAll("-", "")}`;
};
const insertData = async (data: PresetDataEntry[]) => {
if (data.length < 1) return;
const tableName = getTableName(data[0].bookDate);
const out = await surreal.insert<PresetDataEntry>(tableName, data);
console.log(
`[+] Inserted post data in ${tableName} for ${data[0].bookDate} - ${data[0].drawId}`,
);
return out;
};
const getDataByDraw = async (date: string, drawId: number) => {
const tableName = getTableName(date);
const [data] = await surreal.query<[PresetDataEntry[]]>(
`select * from type::table($tableName) where bookDate = $date and drawId = $drawId`,
{ tableName, date, drawId },
);
return data.result || ([] as PresetDataEntry[]);
};
const getDataGroupedBySheetByDraw = async (date: string, drawId: number) => {
const tableName = getTableName(date);
const [data] = await surreal.query<[PresetDataEntry[]]>(
`select * from type::table($tableName) where bookDate = $date and drawId = $drawId`,
{ tableName, date, drawId },
);
const out = {
abData: [] as PresetDataEntry[],
abcData: [] as PresetDataEntry[],
all: data.result || ([] as PresetDataEntry[]),
};
for (const row of data.result ?? []) {
if (row.number.length === 2) {
out.abData.push(row);
} else if (row.number.length === 3) {
out.abcData.push(row);
}
}
return out;
};
async function deleteDataByIds(date: string, ids: string[]) {
const tableName = getTableName(date);
await surreal.query<[PresetDataEntry[]]>(
`delete from type::table($tableName) where id in $ids`,
{ tableName, ids },
);
}
export const dbPresetData = {
insertData,
getDataGroupedBySheetByDraw,
getDataByDraw,
deleteDataByIds,
};

90
src/lib/server/db/user.db.ts Executable file
View File

@@ -0,0 +1,90 @@
import type { User } from "$lib/utils/data.types";
import { surreal, type QueryResult } from "../connectors/surreal.db";
export const dbUser = {
doesExist: async (username?: string) => {
if (username) {
const [rizzult] = await surreal.query<{ count: number }[]>(
"select count() from user where username = $username group all",
{ username: username }
);
if (rizzult.status == "OK") {
return rizzult.result?.count > 0;
}
}
return false;
},
create: async (data: {
username: string;
password: string;
userType: string;
association: string;
}) => {
const doesUserAlreadyExist = await dbUser.doesExist(data.username);
console.log("doesUserAlreadyExist :: ", doesUserAlreadyExist);
if (doesUserAlreadyExist) {
return [{ message: "User already exists." }];
}
const { username, password, association, userType } = data;
const out = await surreal.create<any>(`user:ulid()`, {
createdAt: Date.now().toString(),
updatedAt: Date.now().toString(),
username,
password,
userType,
association,
});
return out as User[];
},
all: async () => {
return await surreal.select<User>("user");
},
get: async (d: {
username?: string;
id?: string;
}): Promise<User | undefined> => {
if (d.id) {
return (await surreal.select<User>(`user:${d.id}`))[0];
}
if (d.username) {
const rizzult = (
await surreal.query<[User[]]>(
`select * from user where username = $username`,
{ username: d.username }
)
)[0];
if (rizzult.status == "OK") {
return rizzult.result[0];
}
}
return undefined;
},
getChildren: async (username?: string) => {
const rizzult = await surreal.query<User[]>(
`select * from user where association = $username`,
{ username: username }
);
return getParsedUsers(rizzult);
},
update: async (id: string, data: { association: string }) => {
const [rizzult] = await surreal.update<User>(`user:${id}`, {
updatedAt: Date.now().toString(),
association: data.association,
} as User);
return rizzult;
},
delete: async (id: string) => {
const out = await surreal.delete(`user:${id}`);
return out[0].id;
},
};
const getParsedUsers = (data: QueryResult<User>[]) => {
const users = [] as User[];
for (const each of data) {
if (each.status == "OK") {
users.push(each.result);
}
}
return users;
};

View File

@@ -0,0 +1,291 @@
import {
COMMISSION_PERCENTAGE,
LEXICODE_MATHCER_PATTERNS,
LEXICODE_PRIZE_PERCENTAGES,
NUMBERS_IN_FIRST_DRAW,
NUMBERS_IN_SECOND_DRAW,
} from "$lib/utils/constants";
import type {
BookingEntry,
FinalSheetData,
FinalSheetRow,
LexiCodeCacheObject,
ReducedFinalSheetData,
ServerError,
} from "$lib/utils/data.types";
import { dbApiData } from "./db/apidata.db";
import { redis } from "./connectors/redis";
import { getULID, getUUID } from "$lib/utils";
export const getCompiledFinalSheet = async (date: string, drawId: string) => {
const bookingEntries = await dbApiData.getBookingEntriesByDraw(date, drawId);
const finalSheet = {
id: getULID(),
date,
drawId,
data: [],
totals: getDefaultTotals(),
} as FinalSheetData;
const lexiCodeCache = await getLexiCodeCache();
const { sheetCache, failed } = await getSheetCache(bookingEntries);
if (failed.length > 0) {
console.log(`[-] Failed to find lexicodes for ${failed.length} entries`);
}
console.log("[...] Now compiling the final sheet");
for (const number of get4DigitGenerator()) {
const fsRow = {
rate: { first: 0, second: 0 },
prize: { first: 0, second: 0 },
profit: { first: 0, second: 0 },
} as FinalSheetRow;
const childNumbers = lexiCodeCache[number];
let vals = getLexiCodeCacheObject(number);
const sheetCacheKeys = Object.keys(sheetCache);
for (const child of childNumbers) {
const lco = getLexiCodeCacheObject(child.number);
if (sheetCacheKeys.includes(child.number)) {
lco.number = child.number;
// TODO: make this into a loop for first and second
vals.frequency.first += sheetCache[child.number].frequency.first;
vals.rate.first += sheetCache[child.number].rate.first;
vals.prize.first += sheetCache[child.number].prize.first;
lco.frequency.first += sheetCache[child.number].frequency.first;
lco.rate.first += sheetCache[child.number].rate.first;
lco.prize.first += sheetCache[child.number].prize.first;
vals.frequency.second += sheetCache[child.number].frequency.second;
vals.rate.second += sheetCache[child.number].rate.second;
vals.prize.second += sheetCache[child.number].prize.second;
lco.frequency.second += sheetCache[child.number].frequency.second;
lco.rate.second += sheetCache[child.number].rate.second;
lco.prize.second += sheetCache[child.number].prize.second;
}
// @ts-ignore
fsRow[child.lexiCode] = lco;
}
fsRow.id = getUUID();
fsRow.number = number;
fsRow.frequency = vals.frequency;
fsRow.rate = { first: vals.rate.first, second: vals.rate.second };
// TODO: MAYBE: recalculate these
fsRow.prize = { first: vals.prize.first, second: vals.prize.second };
fsRow.profit = {
first: calculateProfit(vals.rate.first, vals.prize.first),
second: calculateProfit(vals.rate.second, vals.prize.second),
};
finalSheet.data.push(fsRow);
}
console.log("[+] Final sheet compilation complete");
return { finalSheet, ok: true, errors: [] as ServerError };
};
const getSheetCache = async (bookingEntries: BookingEntry[]) => {
const sheetCache = {} as Record<string, LexiCodeCacheObject>;
const failed = [] as BookingEntry[];
console.log("[...] Preparing the FS final sheet cache");
// INFO: first loop we calculate the vals for each indiv. number of the first and second
for (const entry of bookingEntries) {
const no = entry.number;
const lexiCode = getLexiCode(no);
if (!lexiCode) {
failed.push(entry);
continue;
}
if (sheetCache[no] === undefined) {
sheetCache[no] = getLexiCodeCacheObject(no);
}
sheetCache[no].frequency.first++;
sheetCache[no].rate.first += entry.first;
sheetCache[no].prize.first += calculatePrize(
getRateAfterCommission(sheetCache[no].rate.first),
lexiCode,
"first",
getNoOfDigits(lexiCode),
NUMBERS_IN_FIRST_DRAW
);
sheetCache[no].frequency.second++;
sheetCache[no].rate.second += entry.second;
sheetCache[no].prize.second += calculatePrize(
getRateAfterCommission(sheetCache[no].rate.second),
lexiCode,
"second",
getNoOfDigits(lexiCode),
NUMBERS_IN_SECOND_DRAW
);
}
return { sheetCache, failed };
};
const getDefaultTotals = () => {
return {
commission: { first: 0, second: 0 },
netRate: { first: 0, second: 0 },
rate: { first: 0, second: 0 },
prize: { first: 0, second: 0 },
profit: { first: 0, second: 0 },
frequency: { first: 0, second: 0 },
};
};
const getNoOfDigits = (lexiCode: string) => {
const lens = { 1: 10, 2: 100, 3: 1000, 4: 10000 };
return lens[lexiCode.replaceAll("+", "").length as keyof typeof lens];
};
const getRateAfterCommission = (rate: number) => {
return rate - rate * COMMISSION_PERCENTAGE;
};
const getLexiCodeCache = async () => {
type CacheType = Record<string, { number: string; lexiCode: string }[]>;
const rKey = "lexicodecache";
const found = await redis.get(rKey);
if (found) {
return JSON.parse(found) as CacheType;
}
const cache = {} as CacheType;
for (const number of get4DigitGenerator()) {
cache[number] = getAllMatchingChildNumbers(number);
}
await redis.set(rKey, JSON.stringify(cache));
return cache;
};
const getAllMatchingChildNumbers = (parent: string) => {
const out = [] as { number: string; lexiCode: string }[];
out.push({ number: `${parent[0]}`, lexiCode: "a" });
out.push({ number: `+${parent[0]}`, lexiCode: "+a" });
out.push({ number: `++${parent[0]}`, lexiCode: "++a" });
out.push({ number: `+++${parent[0]}`, lexiCode: "+++a" });
out.push({ number: `${parent[0]}${parent[1]}`, lexiCode: "ab" });
out.push({ number: `+${parent[0]}${parent[1]}`, lexiCode: "+ab" });
out.push({ number: `${parent[0]}+${parent[1]}`, lexiCode: "a+b" });
out.push({ number: `+${parent[0]}+${parent[1]}`, lexiCode: "+a+b" });
out.push({ number: `++${parent[0]}${parent[1]}`, lexiCode: "++ab" });
out.push({ number: `${parent[0]}++${parent[1]}`, lexiCode: "a++b" });
out.push({ number: `${parent[0]}${parent[1]}${parent[2]}`, lexiCode: "abc" });
out.push({
number: `+${parent[0]}${parent[1]}${parent[2]}`,
lexiCode: "+abc",
});
out.push({
number: `${parent[0]}+${parent[1]}${parent[2]}`,
lexiCode: "a+bc",
});
out.push({
number: `${parent[0]}${parent[1]}+${parent[2]}`,
lexiCode: "ab+c",
});
out.push({ number: parent, lexiCode: "abcd" });
return out;
};
function* get4DigitGenerator(): Generator<string> {
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
for (let k = 0; k < 10; k++) {
for (let l = 0; l < 10; l++) {
yield `${i}${j}${k}${l}`;
}
}
}
}
}
const calculatePrize = (
amount: number,
lexiCode: string,
type: "first" | "second",
noOfDigits: number,
noOfDrawNumbers: number
) => {
const lexiCodePercentage =
LEXICODE_PRIZE_PERCENTAGES[type][
lexiCode as keyof typeof LEXICODE_PRIZE_PERCENTAGES.first
];
if (amount && lexiCodePercentage > 0 && noOfDrawNumbers > 0) {
return Number(
(
(amount * noOfDigits * (lexiCodePercentage / 100)) /
noOfDrawNumbers
).toFixed(2)
);
}
return 0;
};
const calculateProfit = (rate: number, prize: number) => {
return getRateAfterCommission(rate) - prize;
};
const getLexiCode = (no: string) => {
for (const [lexicode, pattern] of Object.entries(LEXICODE_MATHCER_PATTERNS)) {
if (pattern.test(no)) {
return lexicode;
}
}
return false;
};
const getLexiCodeCacheObject = (no: string) => {
return {
number: no,
rate: { first: 0, second: 0 },
prize: { first: 0, second: 0 },
frequency: { first: 0, second: 0 },
} as LexiCodeCacheObject;
};
// const getMatchingParents = (no: string) => {
// const matching = [] as string[];
// for (const parent of get4DigitGenerator()) {
// if (doesNumberMatch(parent, no)) {
// matching.push(parent);
// }
// }
// return matching;
// };
// const doesNumberMatch = (parent: string, child: string) => {
// let allMatch = true;
// for (let i = 0; i < child.length; i++) {
// if (parent[i] !== child[i] && child[i] !== "+") {
// allMatch = false;
// }
// }
// return allMatch;
// };
// const getLexiCodeCache = async () => {
// const rKey = "lexicodecache";
// const keyCount = await redis.keys(rKey);
// if (keyCount.length === 1) {
// return JSON.parse((await redis.get(rKey)) ?? "");
// }
// const lexicodeCache: Record<string, string[]> = {};
// for (let i = 0; i < 10; i++) {
// lexicodeCache[i.toString()] = getMatchingParents(i.toString());
// lexicodeCache[`+${i}`] = getMatchingParents(`+${i}`);
// lexicodeCache[`++${i}`] = getMatchingParents(`++${i}`);
// lexicodeCache[`+++${i}`] = getMatchingParents(`+++${i}`);
// for (let j = 0; j < 10; j++) {
// lexicodeCache[`${i}${j}`] = getMatchingParents(`${i}${j}`);
// lexicodeCache[`+${i}${j}`] = getMatchingParents(`+${i}${j}`);
// lexicodeCache[`${i}+${j}`] = getMatchingParents(`${i}+${j}`);
// lexicodeCache[`+${i}+${j}`] = getMatchingParents(`+${i}+${j}`);
// lexicodeCache[`++${i}${j}`] = getMatchingParents(`++${i}${j}`);
// lexicodeCache[`${i}++${j}`] = getMatchingParents(`${i}++${j}`);
// for (let k = 0; k < 10; k++) {
// lexicodeCache[`${i}${j}${k}`] = getMatchingParents(`${i}${j}${k}`);
// lexicodeCache[`+${i}${j}${k}`] = getMatchingParents(`+${i}${j}${k}`);
// lexicodeCache[`${i}+${j}${k}`] = getMatchingParents(`${i}+${j}${k}`);
// lexicodeCache[`${i}${j}+${k}`] = getMatchingParents(`${i}${j}+${k}`);
// for (let l = 0; l < 10; l++) {
// lexicodeCache[`${i}${j}${k}${l}`] = [`${i}${j}${k}${l}`];
// }
// }
// }
// }
// await redis.set(rKey, JSON.stringify(lexicodeCache));
// return lexicodeCache;
// };

View File

@@ -0,0 +1,316 @@
import { getRandomUserAgent, getULID, sleep } from "$lib/utils";
import { constants } from "$lib/utils/constants";
import type { BookingEntry, Draw, LooseApiUser } from "$lib/utils/data.types";
import { rng } from "$lib/utils/rng";
export const testIfSessionIsValid = async (jwt: string) => {
try {
const res = await fetch(
`${constants.SCRAP_API_URL}/v1/user/get-balance?userId=6339`,
{
headers: {
...constants.SCRAP_API_BASE_HEADERS,
Authorization: jwt,
"User-Agent": getRandomUserAgent(),
},
},
);
if (res.status !== 200) {
return false;
}
const rj = (await res.json()) as {
code: number;
success: boolean;
message: string;
data: any;
time: string;
};
return rj.code == 200 && rj.success;
} catch (err) {
console.log(err);
return false;
}
};
export const getSessionToken = async (payload: {
userId: string;
password: string;
verifyToken: string;
code: string;
userType: number;
}): Promise<{ ok: boolean; message: string }> => {
console.log("Requesting...");
const res = await fetch(`${constants.SCRAP_API_URL}/v1/auth/login`, {
method: "POST",
headers: {
...constants.SCRAP_API_BASE_HEADERS,
"User-Agent": getRandomUserAgent(),
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const out = await res.json();
if (out.code !== 200) {
return { ok: false, message: out.message };
}
return { ok: true, message: out.data.token };
};
export async function getUsersBalance(userId: number, jwt: string) {
try {
const res = await fetch(
`${constants.SCRAP_API_URL}/v1/user/get-balance?userId=${userId}`,
{
headers: {
...constants.SCRAP_API_BASE_HEADERS,
Authorization: jwt,
"User-Agent": getRandomUserAgent(),
},
},
);
const rj = (await res.json()) as {
code: number;
success: boolean;
message: string;
data: { allowedBalance: number; balance: number };
time: string;
};
if (res.status !== 200 || rj.code !== 200 || !rj.success) {
console.log(
`[!] Error getting balance for ${userId} :: ${JSON.stringify(rj)}`,
);
return false;
}
return rj.data.balance;
} catch (err) {
console.log(err);
return false;
}
}
export const getDealers = async (jwt: string, distributor_ids: string[]) => {
try {
// // Create an array of promises for each fetch request
const requests = distributor_ids.map(async (did) => {
await sleep(rng(100, 10000));
const res = await fetch(
`${constants.SCRAP_API_URL}/v1/user/dealer-list`,
{
method: "POST",
headers: {
...constants.SCRAP_API_BASE_HEADERS,
"Content-Type": "application/json",
"User-Agent": getRandomUserAgent(),
Authorization: jwt,
},
body: JSON.stringify({
page: 1,
pageSize: 999999,
parentDistributor: parseInt(did),
}),
},
);
const data = (await res.json()) as {
code: number;
success: boolean;
message: string;
data: {
items: any[];
total: number;
};
};
if (data.code !== 200 || !data.success) {
return {
dealers: [],
ok: false,
code: data.code,
message: data.message,
};
}
const dealers = data.data.items.map((item) => item.dealer);
return {
dealers,
ok: res.status === 200 && data.success,
code: data.code,
message: data.message,
};
});
// // Wait for all promises to resolve
const responses = await Promise.all(requests);
const dealers: LooseApiUser[] = [];
const errors: { message: string }[] = [];
for (const res of responses) {
if (res.code !== 200 || !res.ok) {
errors.push({ message: res.message });
continue;
}
for (const dealer of res.dealers) {
dealers.push(dealer);
}
}
// fs.writeFileSync("dealers.json", JSON.stringify(dealers, null, 2));
return { dealers, errors };
} catch (err) {
console.error(err);
return {
dealers: [],
errors: [{ message: "An error occured during fetching dealers" }],
};
}
};
export const getDistributors = async (jwt: string) => {
const res = await fetch(
`${constants.SCRAP_API_URL}/v1/user/distributor-list`,
{
method: "POST",
headers: {
...constants.SCRAP_API_BASE_HEADERS,
Authorization: jwt,
"Content-Type": "application/json",
"User-Agent": getRandomUserAgent(),
},
body: JSON.stringify({
page: 1,
pageSize: 999999,
parentDistributor: 15,
}),
},
);
const json = (await res.json()) as {
code: number;
success: boolean;
message: string;
data: { total: number; items: any[] };
};
if (!json.data.items || json.code !== 200 || !json.success) {
return { ok: false, message: json.message, data: [] };
}
// fs.writeFileSync(
// "distributors.json",
// JSON.stringify(json.data.items, null, 2),
// );
return {
ok: true,
message: "",
data: json.data.items.map((item) => item.distributor),
};
};
export const getDraws = async (jwt: string) => {
const res = await fetch(
`${constants.SCRAP_API_URL}/v1/draw/list-my?userId=15`,
{
method: "GET",
headers: {
...constants.SCRAP_API_BASE_HEADERS,
Authorization: jwt,
"Content-Type": "application/json",
"User-Agent": getRandomUserAgent(),
},
},
);
type J = {
code: number;
success: boolean;
message: string;
data: { draw: Draw }[];
};
let decoded = (await res.json()) as { data: J };
const json = (decoded.data.success ? decoded.data : decoded) as any as J;
if (json.code !== 200 || !json.success || !json.data) {
return { ok: false, message: json.message, data: [] };
}
return {
ok: true,
message: "",
data: json.data.map((item) => item.draw),
};
};
export const getData = async (
jwt: string,
userIds: number[],
drawId: number,
chosenDate: string,
) => {
const res = await fetch(`${constants.SCRAP_API_URL}/v1/book/list2`, {
method: "POST",
headers: {
...constants.SCRAP_API_BASE_HEADERS,
Authorization: jwt,
"Content-Type": "application/json",
"User-Agent": getRandomUserAgent(),
},
body: JSON.stringify({
userType: 3,
userIds,
drawId: drawId,
startDate: chosenDate,
endDate: chosenDate,
beAdmin: false,
containImported: false,
keyword: "",
}),
});
type J = {
code: number;
success: boolean;
message: string;
data: { book: BookingEntry; user: any }[];
};
let decoded = (await res.json()) as { data: J };
const json = (decoded.data.success ? decoded.data : decoded) as any as J;
if (json.code !== 200 || !json.success || !json.data) {
return { ok: false, message: json.message, data: [] };
}
return { ok: true, message: "", data: json.data.map((e) => e.book) };
};
export const mockGetUserData = async (
jwt: string,
userIds: number[],
drawId: number,
chosenDate: string,
) => {
console.log("Rizzzzard of Mogwards!");
const entries = [] as BookingEntry[];
const rng = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
const randomCeil = rng(10_000, 200_000);
await sleep(rng(100, 1000));
for (let i = 0; i < randomCeil; i++) {
const _f = rng(5, 50);
const _s = rng(5, 50);
const f = _f - (_f % 5);
const s = _s - (_s % 5);
entries.push({
id: getULID(),
bookDate: chosenDate,
changedBalance: f + s,
first: f,
second: s,
dealerId: userIds[rng(0, userIds.length - 1)],
distributorId: 6339,
drawId: drawId,
number: rng(0, 9999).toString(),
requestId: new Date().getTime().toString(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
sheetId: "0",
sheetName: "",
});
}
return { ok: true, message: "", data: entries };
};

View File

@@ -0,0 +1,270 @@
import type {
BookingEntry,
FSTotals,
FinalSheetRow,
LexiCodeCacheObject,
ReducedFinalSheetData,
ReducedFinalSheetRow,
ServerError,
} from "$lib/utils/data.types";
import { dbApiData } from "./db/apidata.db";
import { redis } from "./connectors/redis";
import { getDefaultTotals, getULID } from "$lib/utils";
import {
calculatePrize,
calculateProfit,
get4DigitGenerator,
getAllMatchingChildNumbers,
getCommisionAmt,
getLexiCode,
getLexiCodeCacheObject,
getNoOfDigits,
getNetRate,
} from "$lib/utils/finalsheet.utils";
export const getReducedFinalSheet = async (fsData: ReducedFinalSheetData) => {
const bookingEntries = await dbApiData.getBookingEntriesByDraw(
fsData.date,
fsData.drawId
);
console.log(
`[...] Got ${bookingEntries.length} booking entries for ${fsData.date}, draw ${fsData.drawId}`
);
const lexiCodeCache = await getLexiCodeCache();
const { sheetCache, totals, failed } = await getSheetCache(bookingEntries);
const sheetCacheKeys = new Set();
for (const each of Object.keys(sheetCache)) {
sheetCacheKeys.add(each);
}
if (failed.length > 0) {
console.log(`[-] Failed to find lexicodes for ${failed.length} entries`);
console.log(failed.map((e) => e.number));
}
fsData.totals = totals;
console.log("[...] Compiling the final sheet");
// fs.writeFileSync("test.json", JSON.stringify(sheetCache, null, 2));
let highestRate = { first: 0, second: 0 };
for (const number of get4DigitGenerator()) {
const fsRow = {
id: getULID(),
number: number,
rate: { first: 0, second: 0 },
prize: { first: 0, second: 0 },
profit: { first: 0, second: 0 },
frequency: { first: 0, second: 0 },
frequencies: { abcd: { first: 0, second: 0 } },
} as ReducedFinalSheetRow;
const childNumbers = lexiCodeCache[number];
let vals = getLexiCodeCacheObject(number);
for (const child of childNumbers) {
if (!sheetCacheKeys.has(child.number)) {
continue;
}
for (const each of ["first", "second"] as const) {
vals.frequency[each] += sheetCache[child.number].frequency[each];
vals.rate[each] += sheetCache[child.number].rate[each];
vals.prize[each] += sheetCache[child.number].prize[each];
if (child.lexiCode !== "abcd") {
continue;
}
fsRow.frequencies[child.lexiCode][each] =
sheetCache[child.number].frequency[each];
}
}
for (const each of ["first", "second"] as const) {
fsRow.frequency[each] = vals.frequency[each];
fsRow.rate[each] = vals.rate[each];
fsRow.prize[each] = vals.prize[each];
fsRow.profit[each] = calculateProfit(
totals.netRate[each],
vals.prize[each]
);
}
if (fsRow.rate.first > highestRate.first) {
highestRate.first = fsRow.rate.first;
}
if (fsRow.rate.second > highestRate.second) {
highestRate.second = fsRow.rate.second;
}
// console.log(fsRow);
// throw new Error("test");
fsData.data.push(fsRow);
}
console.log("[+] Reduced Final sheet compilation complete");
return { ok: true, errors: [] as ServerError };
};
export const getTargetFSRow = async (
drawId: string,
date: string,
number: string
) => {
const bookingEntries = await dbApiData.getBookingEntriesByDraw(date, drawId);
const lexiCodeCache = await getLexiCodeCache();
const { sheetCache, totals, failed } = await getSheetCache(bookingEntries);
if (failed.length > 0) {
console.log(`[-] Failed to find lexicodes for ${failed.length} entries`);
console.log(failed.map((e) => e.number));
}
console.log("[...] Preparing the FS row");
// fs.writeFileSync("test.json", JSON.stringify(sheetCache, null, 2));
let highestRate = { first: 0, second: 0 };
const fsRow = {
id: getULID(),
number: number,
rate: { first: 0, second: 0 },
prize: { first: 0, second: 0 },
profit: { first: 0, second: 0 },
frequency: { first: 0, second: 0 },
} as FinalSheetRow;
const childNumbers = lexiCodeCache[number];
let vals = getLexiCodeCacheObject(number);
const sheetCacheKeys = Object.keys(sheetCache);
for (const child of childNumbers) {
if (sheetCacheKeys.includes(child.number)) {
for (const each of ["first", "second"] as const) {
vals.frequency[each] += sheetCache[child.number].frequency[each];
vals.rate[each] += sheetCache[child.number].rate[each];
vals.prize[each] += sheetCache[child.number].prize[each];
}
}
}
for (const each of ["first", "second"] as const) {
fsRow.frequency[each] = vals.frequency[each];
fsRow.rate[each] = vals.rate[each];
fsRow.prize[each] = vals.prize[each];
fsRow.profit[each] = calculateProfit(
totals.netRate[each],
vals.prize[each]
);
}
if (fsRow.rate.first > highestRate.first) {
highestRate.first = fsRow.rate.first;
}
if (fsRow.rate.second > highestRate.second) {
highestRate.second = fsRow.rate.second;
}
console.log("[+] FS Row prepared");
return { ok: true, errors: [] as ServerError, data: fsRow };
};
const getSheetCache = async (bookingEntries: BookingEntry[]) => {
const sheetCache = {} as Record<string, LexiCodeCacheObject>;
const totals = getDefaultTotals() as FSTotals;
const failed = [] as BookingEntry[];
console.log("[...] Preparing the pre-FS cache");
// INFO: first loop we calculate the vals for each indiv. number of the first and second
for (const entry of bookingEntries) {
const no = entry.number;
const lexiCode = getLexiCode(no);
if (!lexiCode) {
failed.push(entry);
continue;
}
if (sheetCache[no] === undefined) {
sheetCache[no] = getLexiCodeCacheObject(no);
}
for (const each of ["first", "second"] as const) {
if (entry[each] > 0) {
sheetCache[no].frequency[each]++;
}
sheetCache[no].rate[each] += entry[each];
sheetCache[no].prize[each] = calculatePrize(
sheetCache[no].rate[each],
lexiCode,
each,
getNoOfDigits(lexiCode)
);
}
}
if (bookingEntries.length > 0) {
for (const value of Object.values(sheetCache)) {
for (const each of ["first", "second"] as const) {
totals.frequency[each] += value.frequency[each];
totals.prize[each] += value.prize[each];
totals.rate[each] += value.rate[each];
}
}
for (const each of ["first", "second"] as const) {
totals.commission[each] = getCommisionAmt(totals.rate[each]);
totals.netRate[each] = getNetRate(totals.rate[each]);
}
}
return { sheetCache, failed, totals };
};
const getLexiCodeCache = async () => {
type CacheType = Record<string, { number: string; lexiCode: string }[]>;
const rKey = "lexicodecache";
const found = await redis.get(rKey);
if (found) {
return JSON.parse(found) as CacheType;
}
const cache = {} as CacheType;
for (const number of get4DigitGenerator()) {
cache[number] = getAllMatchingChildNumbers(number);
}
await redis.setex(rKey, 3600 * 24, JSON.stringify(cache));
return cache;
};
const getCachedReducedFinalSheet = async (date: string, drawId: string) => {
const key = `cfinalsheet:${date}:${drawId}`;
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached);
}
return null;
};
const setCachedReducedFinalSheet = async (
date: string,
drawId: string,
data: ReducedFinalSheetData
) => {
const key = `cfinalsheet:${date}:${drawId}`;
await redis.setex(key, 3600 * 24, JSON.stringify(data));
};
export const deleteCachedReducedFinalSheet = async (
date: string,
drawId: string
) => {
await redis.del(`cfinalsheet:${date}:${drawId}`);
};
export const getTestBookingData = async (drawId: string, date: string) => {
const numbers = {} as Record<string, Set<string>>;
for (const each of get4DigitGenerator()) {
const childNumbers = getAllMatchingChildNumbers(each);
for (const child of childNumbers) {
if (numbers[child.lexiCode] === undefined) {
numbers[child.lexiCode] = new Set();
}
numbers[child.lexiCode].add(child.number);
}
}
const out = [] as BookingEntry[];
for (const v of Object.values(numbers)) {
for (const child of v) {
const entry = {
id: getULID(),
drawId: Number(drawId.split(":")[1]),
bookDate: date,
date,
number: child,
first: 10,
second: 10,
dealerId: 4677,
sheetId: "test",
requestId: getULID(),
distributorId: 6339,
changedBalance: 0,
} as BookingEntry;
out.push(entry);
}
}
return out;
};

11
src/lib/server/hashing.ts Executable file
View File

@@ -0,0 +1,11 @@
import bcrypt from "bcryptjs";
const rounds = 10;
export const hashData = (data: string): string => {
return bcrypt.hashSync(data, rounds);
};
export const compareData = (data: string, hash: string): boolean => {
return bcrypt.compareSync(data, hash);
};

View File

@@ -0,0 +1,445 @@
import { getRandomUserAgent, getULID, sleep } from "$lib/utils";
import { constants } from "$lib/utils/constants";
import type {
ApiPostUserWithParent,
APISession,
Draw,
PostDataEntry,
ServerError,
} from "$lib/utils/data.types";
import Fetch from "node-fetch";
import { HttpsProxyAgent } from "https-proxy-agent";
export type APIRespnose<T> = {
code: number;
success: boolean;
message: string;
data: T;
time: string;
};
export function buildMessageString(
i: number,
rows: PostDataEntry[],
distributorId: number,
dealerId: number,
drawId: number,
date: string,
) {
let message = "";
let jumpSize = Math.floor(Math.random() * 490) + 10;
let total = 0;
let startReqId = new Date().getTime();
let x = 0;
for (let j = i; j < i + jumpSize; j++) {
if (j >= rows.length) {
break;
}
const row = rows[j];
const reqId = startReqId + x++;
const no = row.number.trim();
const f = row.first;
const s = row.second;
const mTotal = f + s;
if (mTotal <= 0) {
continue;
}
total += mTotal;
message += `${reqId},${distributorId},${dealerId},${drawId},${date},${no},${f},${s},${mTotal};`;
}
message = message.slice(0, -1);
return { message, total, jumped: i + jumpSize };
}
export async function postDataToApi(payload: {
sessions: Record<string, APISession>;
draw: Draw;
data: PostDataEntry[];
users: ApiPostUserWithParent[];
}) {
const responses = [] as APIRespnose<[]>[];
const responsesIds = [] as { requestId: number; bookId: string }[];
let failedResponses = 0;
let successResponses = 0;
console.log(`[+] Sending ${payload.data.length} requests...`);
const dataByUser = {} as Record<string, PostDataEntry[]>;
for (const row of payload.data) {
const userId = row.userId ?? "";
if (userId.length < 1) {
console.log(`[!] User not found for request ${row.userId}`);
return {
ok: false,
detail: "User not found to post data with",
errors: [{ message: "User not found for request" }] as ServerError,
};
}
if (!dataByUser[userId]) {
dataByUser[userId] = [];
}
dataByUser[userId].push(row);
}
try {
for (const userId in dataByUser) {
const session = payload.sessions[userId];
const usr = payload.users.find((u) => u.userId === userId);
if (!usr) {
console.log(`[!] User ${userId} not found for posting to api`);
return {
ok: false,
detail: "User not found to post data with",
errors: [{ message: "User not found for request" }] as ServerError,
};
}
const distId = usr.parentDistributor ?? 0;
const dealerId = Number(session.userId.split(":")[1]);
const drawId = Number(payload.draw.id.split(":")[1]);
const date = new Date().toISOString().split("T")[0];
let i = 0;
while (i < dataByUser[userId].length) {
let tries = 0;
while (tries < 3) {
let { message, total, jumped } = buildMessageString(
i,
dataByUser[userId],
distId,
dealerId,
drawId,
date,
);
const res = await sendBatchRequest(
session,
dealerId,
payload.draw,
total,
message,
);
const rj = (await res.json()) as APIRespnose<{
bookDtos: { bookId: string; requestId: number }[];
}>;
if (rj.code === 200 && res.status === 200) {
i = jumped;
responsesIds.push(
...rj.data.bookDtos.map((b) => ({
requestId: b.requestId as number,
bookId: b.bookId as string,
})),
);
successResponses++;
break;
}
failedResponses++;
tries++;
}
if (tries >= 3) {
console.log(
`[!] Failed to send data to api for user ${userId}, deleting all booked entries...`,
);
console.log(responsesIds);
if (responsesIds.length > 0) {
const out = await deleteAllBookedEntries({
data: responsesIds,
closeTime: payload.draw.closeTime,
dealerId,
drawId,
session,
});
console.log(await out.text());
}
return {
ok: false,
detail: "Failed to post data to API halfway through",
errors: [
{ message: "Failed to post data to API halfway through" },
] as ServerError,
};
}
}
}
console.log(`[+] Finished sending ${payload.data.length} requests`);
console.log(`[?] Failed responses: ${failedResponses}`);
console.log(`[?] Success responses: ${successResponses}`);
return {
ok: true,
detail: "Successfully sent data to api",
data: responses,
};
} catch (err) {
console.log(err);
return {
ok: false,
detail: "Failed to send data to api",
};
}
}
async function sendBatchRequest(
session: APISession,
dealerId: number,
draw: Draw,
changedBalance: number,
body: string,
) {
return Fetch(`${constants.SCRAP_API_URL}/v1/book/add-multiple`, {
agent: new HttpsProxyAgent(`http://${session.ip}`),
method: "POST",
headers: {
...constants.SCRAP_API_BASE_HEADERS,
"Content-Type": "application/json;charset=UTF-8",
Authorization: session.sessionToken,
"User-Agent": getRandomUserAgent(),
},
body: JSON.stringify({
changedBalance,
closeTime: draw.closeTime,
date: new Date().toISOString().split("T")[0],
dealerId,
drawId: Number(draw.id.split(":")[1]),
insertData: body,
}),
});
}
async function mockSendBatchRequest(
session: APISession,
dealerId: number,
draw: Draw,
changedBalance: number,
body: string,
) {
// between 5 to 20 ms
await sleep(Math.floor(Math.random() * 1000) + 50);
if (Math.random() < 0.005) {
return new Response(
JSON.stringify({
code: 500,
success: false,
message: "Failed",
data: {},
time: new Date().toISOString(),
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
statusText: "Failed",
},
);
}
return new Response(
JSON.stringify({
code: 200,
success: true,
message: "Success",
data: {
bookDtos: body.split(";").map((e) => {
return {
bookId: getULID(),
requestId: +e.split(",")[0],
};
}),
},
time: new Date().toISOString(),
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
statusText: "OK",
},
);
}
async function sendRequest(
requestId: number,
session: APISession,
body: PostDataEntry,
dealerId: number,
distributorId: number,
draw: Draw,
) {
return Fetch(`${constants.SCRAP_API_URL}/v1/book/add`, {
agent: new HttpsProxyAgent(`http://${session.ip}`),
method: "POST",
headers: {
...constants.SCRAP_API_BASE_HEADERS,
"Content-Type": "application/json;charset=UTF-8",
Authorization: session.sessionToken,
"User-Agent": getRandomUserAgent(),
},
body: JSON.stringify({
retryIndex: 0,
requestId: requestId,
date: new Date().toISOString().split("T")[0],
drawId: Number(draw.id.split(":")[1]),
closeTime: draw.closeTime,
dealerId: dealerId,
distributorId: distributorId,
number: body.number,
first: body.first,
second: body.second,
changedBalance: body.first + body.second,
}),
});
}
async function mockSendRequest(
requestId: number,
session: APISession,
body: PostDataEntry,
dealerId: number,
distributorId: number,
draw: Draw,
) {
// between 5 to 15 ms
await sleep(Math.floor(Math.random() * 10 + 5));
// // simulate a failed response, 20% of the time
if (Math.random() < 0.05) {
// return a failed response
return new Response(
JSON.stringify({
code: 500,
success: false,
message: "Failed",
data: {},
time: new Date().toISOString(),
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
statusText: "Failed",
},
);
}
return new Response(
JSON.stringify({
code: 200,
success: true,
message: "Success",
data: {},
time: new Date().toISOString(),
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
statusText: "OK",
},
);
}
async function deleteAllBookedEntries({
session,
data,
dealerId,
drawId,
closeTime,
}: {
session: APISession;
data: { bookId: string; requestId: number }[];
dealerId: number;
drawId: number;
closeTime: string;
}) {
return Fetch(`${constants.SCRAP_API_URL}/v1/book/delete-multiple`, {
agent: new HttpsProxyAgent(`http://${session.ip}`),
method: "POST",
headers: {
...constants.SCRAP_API_BASE_HEADERS,
"Content-Type": "application/json;charset=UTF-8",
Authorization: session.sessionToken,
"User-Agent": getRandomUserAgent(),
},
body: JSON.stringify({
dealerId,
drawId,
closeTime,
bookIds: data.map((e) => e.bookId),
}),
});
}
// export async function postDataToApi(payload: {
// sessions: Record<string, APISession>;
// draw: Draw;
// data: PostDataEntry[];
// users: ApiPostUserWithParent[];
// }) {
// const MAX_CONCURRENT_REQUESTS = 20;
// const responses = [] as APIRespnose<{}>[];
// const baseReqId = new Date().getTime();
// const reqIds = Array.from(
// { length: payload.data.length },
// (_, i) => baseReqId + i,
// );
// let failedResponses = 0;
// let successResponses = 0;
// console.log(`[+] Sending ${payload.data.length} requests...`);
// // "1723223505822,6339,6352,16,2024-08-09,123,10,10,20;1723223505823,6339,6352,16,2024-08-09,12,10,10,20",
// async function processBatch(batch: PostDataEntry[], indexes: number[]) {
// const promises = batch.map(async (row, idx) => {
// await sleep(Math.floor(Math.random() * 10 + 10));
// const session = payload.sessions[row.userId ?? ""];
// const usr = payload.users.find((u) => u.userId === row.userId);
// if (!usr) {
// return null;
// }
// let ok = false,
// tries = 0;
// while (!ok && tries < 3) {
// await sleep(Math.floor(Math.random() * 10 + 10));
// const res = await mockSendRequest(
// reqIds[indexes[idx]],
// session,
// row,
// Number(session.userId.split(":")[1]),
// usr?.parentDistributor,
// payload.draw,
// );
// let rj: APIRespnose<{}>;
// try {
// rj = (await res.json()) as APIRespnose<{}>;
// } catch (err) {
// console.log(err);
// tries++;
// continue;
// }
// if (res.status !== 200 || rj.code !== 200) {
// console.log(
// `Failed to send request ${reqIds[indexes[idx]]}, try ${tries}`,
// );
// tries++;
// continue;
// }
// ok = true;
// successResponses++;
// return rj;
// }
// failedResponses++;
// return null;
// });
// const results = await Promise.all(promises);
// for (const result of results) {
// if (!result) continue;
// responses.push(result);
// }
// }
// try {
// for (let i = 0; i < payload.data.length; i += MAX_CONCURRENT_REQUESTS) {
// const batch = payload.data.slice(i, i + MAX_CONCURRENT_REQUESTS);
// await processBatch(
// batch,
// batch.map((_, x) => i + x),
// );
// }
// console.log(`[+] Finished sending ${payload.data.length} requests`);
// console.log(`[?] Failed responses: ${failedResponses}`);
// console.log(`[?] Success responses: ${successResponses}`);
// return responses;
// } catch (err) {
// console.log(err);
// return false;
// }
// }

View File

@@ -0,0 +1,447 @@
import { dbApiPostData } from "$lib/server/db/apipostdata.db";
import { dbApiUser } from "$lib/server/db/apiuser.db";
import { getReducedFinalSheet } from "$lib/server/finalsheet.helpers";
import {
adjustRatesIfDuplicatesFound,
pairRatesWithNumbers,
removeNumbersWithRepeatingDigits,
splitRatesIntoSmallerForRowsWithLargerRates,
spreadRatesForNumbersBetweenUsers,
} from "$lib/server/postdata/postdata.gen.helpers";
import { getAllSessions } from "$lib/server/utils/session.service";
import { getDefaultTotals, getULID } from "$lib/utils";
import type {
ApiPostUser,
ApiPostUserWithParent,
PresetDataEntry,
PostDataEntry,
PostDataFilters,
PostDataHistoryFilters,
ReducedFinalSheetData,
ReducedFinalSheetRow,
ServerError,
} from "$lib/utils/data.types";
import { dbPresetData } from "$lib/server/db/presetdata.db";
import { getUsersBalance } from "$lib/server/external/api.scraping.helpers";
function filterMatching(
data: ReducedFinalSheetRow[],
min: number,
max: number,
sheetType: "first" | "second",
) {
let abNums = new Set<string>();
let abcNums = new Set<string>();
for (const row of data) {
if (row.prize[sheetType] >= min && row.prize[sheetType] <= max) {
abNums.add(`${row.number[0]}${row.number[1]}`);
abcNums.add(`${row.number[0]}${row.number[1]}${row.number[2]}`);
}
}
return { abNums, abcNums };
}
export async function updateBalanceOfPostUsers(users: ApiPostUserWithParent[]) {
const sessions = await getAllSessions();
const balances = [] as { id: string; balance: number }[];
for (const user of users) {
const session = sessions.find((e) => e.value.userId === user.id);
const jwt = session?.value.sessionToken;
if (!jwt) {
return {
ok: false,
detail: `Session not found for user ${user.userId}`,
};
}
const out = await getUsersBalance(+user.id.split(":")[1], jwt);
if (!out) {
return {
ok: false,
detail: `Error fetching balance for user ${user.userName}`,
};
}
balances.push({ id: user.id, balance: out });
}
await dbApiUser.updatePostUsersBalances(balances);
return {
ok: true,
detail: "",
data: users.map((u) => {
const bal = balances.find((b) => b.id === u.id);
if (!bal) {
console.log(`ERROR: Balance not found for user ${u.userName}`);
}
return { ...u, balance: bal?.balance ?? 0 };
}),
};
}
export async function fetchDataForPosting(
date: string,
input: PostDataFilters,
users: ApiPostUser[],
) {
console.log(`The input ${JSON.stringify(input, null, 2)}`);
const { minPrize, maxPrize } = input;
const draw = input.draw!;
const fsData = {
id: getULID(),
date,
drawId: draw.id,
data: [],
totals: getDefaultTotals(),
} as ReducedFinalSheetData;
if (!draw) {
return {
ok: false,
detail: `Draw for the passed draw ID not found`,
data: [],
users: [],
errors: [
{ message: `Draw for the passed draw ID not found` },
] as ServerError,
};
}
const data = await getReducedFinalSheet(fsData);
if (!data.ok) {
return {
ok: false,
detail: `Error compiling final sheet`,
data: [],
users: [],
errors: data.errors,
};
}
console.log("[+] Filtering the fs data to get the numbers");
const filteredF = filterMatching(fsData.data, minPrize, maxPrize, "first");
console.log(
`Filtered data: ${filteredF.abNums.size}; ${filteredF.abcNums.size}`,
);
// ------------------------------------------
let _abNums = new Set<string>(),
_abcNums = new Set<string>();
for (const each of filteredF.abNums) _abNums.add(each);
for (const each of filteredF.abcNums) _abcNums.add(each);
let abNums = Array.from(_abNums),
abcNums = Array.from(_abcNums);
if (draw.filterDuplicatesWhilePosting === true) {
console.log(`[+] Removing numbers that have repeating digits`);
console.log(`[=] Original : AB: ${abNums.length}, ABC: ${abcNums.length}`);
abNums = removeNumbersWithRepeatingDigits(abNums);
abcNums = removeNumbersWithRepeatingDigits(abcNums);
}
console.log(`[=] AB: ${abNums.length}, ABC: ${abcNums.length}`);
console.log(`Fetching preset data`);
const presetData = await dbPresetData.getDataGroupedBySheetByDraw(
date,
+draw.id.split(":")[1],
);
console.log(`${presetData.all.length} preset entries found`);
for (let tries = 0; tries < 3; tries++) {
console.log(`[✍️] Try ${tries + 1} of generating the result`);
const out = await generatePostDataArrayFromBaseInfo(
input,
users,
abNums,
abcNums,
presetData,
);
if (out.ok) {
return out;
}
if (out.detail.includes("Not enough balance")) {
return {
ok: false,
detail: `Users don't have enough balance to post the data, try reducing the rates`,
data: [],
users: [],
};
}
}
return {
ok: false,
detail: `Could not generate data, please try adjusting the filters`,
data: [],
};
}
export async function generatePostDataArrayFromBaseInfo(
input: PostDataFilters,
users: ApiPostUser[],
abNums: string[],
abcNums: string[],
presetData: {
all: PostDataEntry[];
abData: PresetDataEntry[];
abcData: PresetDataEntry[];
},
) {
console.log("[+] Spreading the rates for the numbers for all post user");
const abData = splitRatesIntoSmallerForRowsWithLargerRates(
spreadRatesForNumbersBetweenUsers(
adjustRatesIfDuplicatesFound(
pairRatesWithNumbers(abNums, input.twoDigitRates),
presetData.abData,
),
users.map((u) => u.userId),
),
);
const abcData = splitRatesIntoSmallerForRowsWithLargerRates(
spreadRatesForNumbersBetweenUsers(
adjustRatesIfDuplicatesFound(
pairRatesWithNumbers(abcNums, input.threeDigitRates),
presetData.abcData,
),
users.map((u) => u.userId),
),
);
// ------------------------------------------
console.log(`[+] Adding ${abData.length} ab entries to final list`);
console.log(`[+] Adding ${abcData.length} abc entries to final list`);
const result = [] as PostDataEntry[];
const alreadyPresent = new Set<string>();
for (const each of abData) {
alreadyPresent.add(each.number);
result.push(each);
}
for (const each of abcData) {
alreadyPresent.add(each.number);
result.push(each);
}
// ------------------------------------------
const balanceCounts = {} as Record<string, number>;
for (const each of result) {
const uid = each.userId ?? "";
if (balanceCounts[uid] === undefined) {
balanceCounts[uid] = 0;
}
balanceCounts[uid] += each.first + each.second;
}
// ------------------------------------------
console.log(
`[+] Appending up to ${presetData.all.length} entries that are not ab, abc`,
);
for (const entry of presetData.all) {
if (
alreadyPresent.has(entry.number) ||
(entry.first < 5 && entry.second < 5)
) {
continue;
}
const randomUserId = users[Math.floor(Math.random() * users.length)].userId;
if (balanceCounts[randomUserId] === undefined) {
balanceCounts[randomUserId] = 0;
}
balanceCounts[randomUserId] += entry.first + entry.second;
result.push({ ...entry, userId: randomUserId });
}
// ------------------------------------------
const usersTotalbalance = users.reduce((a, b) => a + (b.balance ?? 0), 0);
let totalAmtForPostingData = Object.values(balanceCounts).reduce(
(acc, curr) => acc + curr,
0,
);
if (usersTotalbalance < totalAmtForPostingData) {
return {
ok: false,
detail: `Not enough balance to book overall with ${usersTotalbalance} < ${totalAmtForPostingData}`,
data: [],
users: [],
errors: [
{ message: `Not enough balance to book overall` },
] as ServerError,
};
}
function isDistributionUnbalanced() {
let out = false;
for (const key in balanceCounts) {
if (
balanceCounts[key] > (users.find((u) => u.userId === key)?.balance ?? 0)
) {
out = true;
break;
}
}
return out;
}
for (let tries = 0; tries < 5; tries++) {
console.log(
`Balance counts start : ${JSON.stringify(balanceCounts, null, 2)}`,
);
rebalancePostDataListByBalanceOfUsers(balanceCounts, users, result);
console.log(`Balance counts final : ${JSON.stringify(balanceCounts)}`);
let totalAmtForPostingDataAfterRebalance = Object.values(
balanceCounts,
).reduce((acc, curr) => acc + curr, 0);
console.log(
`Total amount for posting data after rebalance: ${totalAmtForPostingDataAfterRebalance}`,
`Total balance of users: ${JSON.stringify(users.map((u) => ({ un: u.userName, b: u.balance })))}`,
);
if (!isDistributionUnbalanced()) {
console.log(`[+] Distribution is balanced`);
break;
}
console.log(`[!] Rebalancing again`);
}
if (isDistributionUnbalanced()) {
return {
ok: false,
detail: `Please regenerate dataset as the some users have not enough balance to book their entries`,
data: [],
users: [],
};
}
// ------------------------------------------
console.log(`[+] Shuffling ${result.length} entries for posting`);
shuffleArray(result);
return {
ok: true,
detail: `Fetched the data successfully`,
data: result,
users,
errors: undefined,
};
}
function shuffleArray<T>(array: T[]): T[] {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
export function rebalancePostDataListByBalanceOfUsers(
balanceCounts: Record<string, number>,
users: ApiPostUser[],
result: PostDataEntry[],
) {
console.log(
`[+] Checking if the users have enough balance to book their assigned data`,
);
for (const user of users) {
const usersBalance = user.balance ?? 0;
const dueForUser = balanceCounts[user.userId] ?? 0;
if (usersBalance === 0) {
console.log(`\n[!] ${user.userName} has no balance\n`);
continue;
}
if (usersBalance >= dueForUser) {
console.log(
`[✅] ${user.userName} can book the data of ${usersBalance} > ${dueForUser} `,
);
continue;
}
console.log(
`[!❎!] ${user.userName} can't book it all ${usersBalance} < ${dueForUser}`,
);
const difference = dueForUser - usersBalance;
let differenceLeft = Number(difference); // make a copy
const entriesToMove = result
.filter((r) => {
if (r.userId === user.userId && differenceLeft > 0) {
differenceLeft -= r.first + r.second;
return true;
}
})
.map((r) => r.id);
console.log(`Have to move ${entriesToMove.length} entries to other users`);
// find a user who has enough balance
const userWithEnoughBalance = users.find((u) => {
return (
(u.balance ?? 0) - balanceCounts[u.userId] >= difference &&
u.userId !== user.userId
);
});
if (!userWithEnoughBalance) {
return {
ok: false,
detail: `No user found with enough balance to cover balance shortage of ${difference} for ${user.userName}`,
data: [],
};
}
console.log(
`Dude has enough balance to take on this other user's expenses ': ${JSON.stringify(userWithEnoughBalance)}`,
);
for (let i = 0; i < result.length; i++) {
if (!entriesToMove.includes(result[i].id)) {
continue;
}
const entry = result[i];
let amountMoved = 0;
if (entry.userId !== user.userId) {
continue;
}
entry.userId = userWithEnoughBalance.userId;
balanceCounts[userWithEnoughBalance.userId] += entry.first + entry.second;
balanceCounts[user.userId] -= entry.first + entry.second;
amountMoved += entry.first + entry.second;
if (amountMoved >= difference) {
// don't move more than the difference'
break;
}
}
console.log(
`[+] Moved ${entriesToMove.length} entries to ${userWithEnoughBalance.userName}`,
);
}
}
export async function fetchPostDataHistory(input: PostDataHistoryFilters) {
const { draw, date } = input;
console.log(`Fetching post data from HISTORY for draw: ${date} - ${draw.id}`);
const found = await dbApiPostData.getPostDataByDraw(date, draw.id);
if (!found) {
return { data: [], users: [], ok: false, detail: "Data not found" };
}
console.log(
`Data found for the passed draw: ${date} - ${draw.id}, returning that`,
);
const users = await dbApiUser.getAllPostUsers();
const uniqueUserIds = [] as string[];
for (const each of found) {
if (!each.userId || uniqueUserIds.includes(each.userId)) {
continue;
}
uniqueUserIds.push(each.userId);
}
return {
data: found,
users: users.filter((u) => uniqueUserIds.includes(u.userId)),
ok: true,
detail: "Data found",
};
}

View File

@@ -0,0 +1,213 @@
import { getULID } from "$lib/utils";
import type {
PostDataEntry,
FSPair,
PresetDataEntry,
} from "$lib/utils/data.types";
function splitRatesRelativelyEvenly(
totalRate: number,
parts: number,
): number[] {
if (totalRate < 5) {
return Array.from({ length: parts }, () => 0);
}
if (totalRate === 5) {
return Array.from({ length: parts }, (_, i) => (i === 0 ? 5 : 0));
}
if (totalRate % 5 !== 0) {
throw new Error("Total rate must be a multiple of 5");
}
const splits: number[] = [];
let remainingRate = totalRate;
// Distribute the rate using a weighted random approach
for (let i = 0; i < parts; i++) {
if (i === parts - 1) {
splits.push(remainingRate);
} else {
const minRate = 5; // Minimum rate for each part
const maxRate = Math.min(
remainingRate - (parts - i - 1) * minRate,
remainingRate * 0.6,
);
const rate =
Math.floor((Math.random() * (maxRate - minRate + 1)) / 5) * 5 + minRate;
splits.push(rate);
remainingRate -= rate;
}
}
// Shuffle the array
for (let i = splits.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[splits[i], splits[j]] = [splits[j], splits[i]];
}
// Occasionally allow for extreme distributions, but ensure at least one non-zero value
if (Math.random() < 0.05) {
// 5% chance
const extremeIndex = Math.floor(Math.random() * parts);
splits.fill(0);
splits[extremeIndex] = totalRate;
}
if (splits.reduce((a, b) => a + b, 0) !== totalRate) {
throw new Error("Splitting error");
}
return splits;
}
export function pairRatesWithNumbers(numbers: string[], rates: FSPair) {
const out = [];
for (let i = 0; i < numbers.length; i++) {
out.push({
id: getULID(),
number: numbers[i].toString(),
first: rates.first,
second: rates.second,
createdAt: new Date().toISOString(),
});
}
return out;
}
export function adjustRatesIfDuplicatesFound(
original: PostDataEntry[],
newRows: PresetDataEntry[],
) {
const originalNumbers = original.map((e) => e.number);
const newNumbers = newRows.map((e) => e.number);
const duplicates = originalNumbers.filter((n) => newNumbers.includes(n));
if (duplicates.length === 0) {
return original;
}
const out = [] as PostDataEntry[];
for (const entry of original) {
if (!duplicates.includes(entry.number)) {
out.push(entry);
continue;
}
let first = entry.first;
let second = entry.second;
// now add rates from newRows
const newRow = newRows.find((e) => e.number === entry.number);
if (newRow) {
first += newRow.first;
second += newRow.second;
}
out.push({ ...entry, first, second });
}
return out;
}
export function spreadRatesForNumbersBetweenUsers(
originalData: PostDataEntry[],
userIds: string[],
) {
const dataWithSplitRates = [] as { first: number[]; second: number[] }[];
for (const entry of originalData) {
const { first, second } = entry;
if (first % 5 !== 0 || second % 5 !== 0) {
throw new Error("Rates must be multiples of 5");
}
const fRates = splitRatesRelativelyEvenly(first, userIds.length);
const sRates = splitRatesRelativelyEvenly(second, userIds.length);
dataWithSplitRates.push({ first: fRates, second: sRates });
}
const out = [] as PostDataEntry[];
for (let uIdx = 0; uIdx < userIds.length; uIdx++) {
let rIdx = 0;
for (const entry of originalData) {
let f = dataWithSplitRates[rIdx].first[uIdx] ?? 0;
let s = dataWithSplitRates[rIdx].second[uIdx] ?? 0;
if (f > 0 || s > 0) {
out.push({
...entry,
first: f,
second: s,
userId: userIds[Math.floor(Math.random() * userIds.length)],
});
}
rIdx++;
}
}
return out;
}
export function splitRatesIntoSmallerForRowsWithLargerRates(
data: PostDataEntry[],
) {
const out = [] as PostDataEntry[];
function getRNBtw1And6(n: number) {
const quotient = n / 5;
return Math.floor(((Math.random() * quotient) % 6) + 1);
}
for (const entry of data) {
if (entry.first < 5 && entry.second < 5) {
continue;
} else if (entry.first === 5 && entry.second === 5) {
out.push(entry);
continue;
}
const firstSplit = splitRatesRelativelyEvenly(
entry.first,
getRNBtw1And6(entry.first),
);
const secondSplit = splitRatesRelativelyEvenly(
entry.second,
getRNBtw1And6(entry.second),
);
const maxLength = Math.max(firstSplit.length, secondSplit.length);
const firstPadded = firstSplit.concat(
Array(maxLength - firstSplit.length).fill(0),
);
const secondPadded = secondSplit.concat(
Array(maxLength - secondSplit.length).fill(0),
);
// shuffle the two
for (let i = firstPadded.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[firstPadded[i], firstPadded[j]] = [firstPadded[j], firstPadded[i]];
const k = Math.floor(Math.random() * (i + 1));
[secondPadded[i], secondPadded[k]] = [secondPadded[k], secondPadded[i]];
}
for (let i = 0; i < maxLength; i++) {
const f = firstPadded[i];
const s = secondPadded[i];
if (f < 5 && s < 5) {
continue;
}
out.push({ ...entry, first: f, second: s });
}
}
// verify that all original rates are preserved, and that the sum of the new rates is equal to the sum of the original rates
const originalSum = data.reduce(
(acc, curr) => acc + curr.first + curr.second,
0,
);
const newSum = out.reduce((acc, curr) => acc + curr.first + curr.second, 0);
if (originalSum !== newSum) {
console.log(
`[---] Original and new sums are not matching at all (${originalSum} !== ${newSum})`,
);
throw new Error("Sum of rates is not equal");
}
return out;
}
export function removeNumbersWithRepeatingDigits(nos: string[]): string[] {
const out = new Set<string>();
for (const no of nos) {
if (new Set(no).size === no.length) {
out.add(no);
}
}
return Array.from(out);
}

103
src/lib/server/session.helpers.ts Executable file
View File

@@ -0,0 +1,103 @@
import { redis } from "./connectors/redis";
import { getUUID } from "$lib/utils";
import type { Session } from "$lib/utils/data.types";
export const defaultTTL = 60 * 60 * 6;
export const generateSession = async (
username: string,
userType: string,
ip: string,
userAgent: string,
ttl?: number,
) => {
ttl = ttl || defaultTTL;
const sId = getUUID();
await deleteMatchingSessions(username, ip, userAgent);
await redis.setex(
sId,
ttl,
ip + "|" + userAgent + "|" + username + "|" + userType + "|" + sId,
);
await redis.sadd("session:" + username, sId);
return sId;
};
export const getSession = async (sId: string) => {
const session = await redis.get(sId);
if (!session) return null;
return parseSession(session);
};
export const getSessions = async (username: string) => {
const sIds = await redis.smembers("session:" + username);
if (!sIds) return null;
const _sessions = await redis.mget(...sIds);
const sessions = [];
for (const session of _sessions) {
if (!session) continue;
sessions.push(parseSession(session));
}
};
export const isSessionValid = async (
sId: string,
ip: string,
userAgent: string,
) => {
const s = await getSession(sId);
if (!s) return false;
if (s.ip !== ip && s.userAgent === userAgent) return false;
if (s.userAgent !== userAgent && s.ip === ip) return false;
if (s.userAgent !== userAgent && s.ip !== ip) return false;
return true;
};
export const isAlreadyLoggedIn = async (
username: string,
ip: string,
userAgent: string,
) => {
const sIds = await redis.smembers("session:" + username);
if (!sIds) return false;
for (const sId of sIds) {
if (await isSessionValid(sId, ip, userAgent)) return true;
}
return false;
};
export const deleteSession = async (sId: string) => {
const session = await getSession(sId);
if (!session) return false;
await redis.del(sId);
await redis.srem("session:" + session.username, sId);
return true;
};
export const deleteMatchingSessions = async (
username: string,
ip: string,
userAgent: string,
) => {
const sIds = await redis.smembers("session:" + username);
if (!sIds) return false;
for (const sId of sIds) {
if (await isSessionValid(sId, ip, userAgent)) {
await deleteSession(sId);
}
}
return true;
};
export const deleteAllSessions = async (username: string) => {
const sIds = await redis.smembers("session:" + username);
if (!sIds) return false;
await redis.del(...sIds);
await redis.del("session:" + username);
return true;
};
const parseSession = (session: string) => {
const [ip, userAgent, username, userType, sId] = session.split("|");
return { sId, ip, userAgent, username, userType } as Session;
};

View File

View File

@@ -0,0 +1,63 @@
import { pickRandomIP } from "$lib/utils";
import { constants } from "$lib/utils/constants";
import type { APISession } from "$lib/utils/data.types";
import { redis } from "../connectors/redis";
import { testIfSessionIsValid } from "../external/api.scraping.helpers";
export async function getSessionFromStore(sid: string) {
const out = await redis.get(sid);
if (out === null) {
return;
}
return JSON.parse(out) as APISession;
}
export async function setSessionToRedis(sessionKey: string, userId: string) {
let key = constants.SCRAP_API_SESSION_KEY;
if (userId) {
key = `apisession:${userId}`;
}
console.log("Setting session to redis", key, sessionKey);
const session: APISession = {
ip: pickRandomIP(),
sessionToken: sessionKey,
userId,
};
await redis.setex(key, 86400, JSON.stringify(session));
}
export async function isSessionValidInStore(userId?: string) {
let key = constants.SCRAP_API_SESSION_KEY;
if (userId) {
key = `apisession:${userId}`;
}
try {
const value = JSON.parse((await redis.get(key)) ?? "") as APISession | null;
if (value === null) {
return { valid: false };
}
return await testIfSessionIsValid(value.sessionToken);
} catch (err) {
return false;
}
}
export async function removeSessionFromStore(userId?: string) {
try {
let key = constants.SCRAP_API_SESSION_KEY;
if (userId) {
key = `apisession:${userId}`;
}
await redis.del(key);
} catch (err) {}
}
export async function getAllSessions() {
const keys = await redis.keys("apisession:*");
const sessions = [];
for (const key of keys) {
const value = JSON.parse((await redis.get(key)) ?? "{}") as APISession;
sessions.push({ key, value });
}
return sessions;
}

75
src/lib/stores/booking.state.ts Executable file
View File

@@ -0,0 +1,75 @@
import type { Draw, BookingEntry } from "$lib/utils/data.types";
import { writable } from "svelte/store";
export type SyncState = {
data: BookingEntry[];
isSyncing: boolean;
lastSynced: string;
};
export const syncState = writable<SyncState>({
data: [],
isSyncing: false,
lastSynced: new Date().toLocaleString(),
});
export function setSyncState(state: SyncState) {
syncState.update((old) => {
return { ...old, ...state };
});
setSyncStateToLocalStorage(state);
}
export function removeEntriesFromSyncState(ids: string[]) {
syncState.update((old) => {
return {
isSyncing: false,
lastSynced: new Date().toISOString(),
data: old.data.filter((entry) => !ids.includes(entry.id)),
};
});
}
export function setSyncStateToLocalStorage(state: SyncState) {
window.localStorage.setItem("syncstate", JSON.stringify(state));
}
export function getSyncStateFromLocalStorage() {
const out = JSON.parse(
// @ts-ignore
window.localStorage.getItem("syncstate") || null
) as SyncState | null;
return out;
}
export const bookingPanelData = writable<BookingEntry[]>([]);
export const selectedEntriesMap = writable({} as Record<string, boolean>);
export const bookingPanelState = writable({
chosenDraw: {} as Draw,
chosenScheme: "",
isPermutationModeSelected: false,
keepRatesPostBookingSubmit: false,
});
type SetBookingPanelStatePayload = {
chosenDraw?: Draw;
chosenScheme?: string;
isPermutationModeSelected?: boolean;
keepRatesPostBookingSubmit?: boolean;
};
export const setBookingPanelState = (state: SetBookingPanelStatePayload) => {
bookingPanelState.update((older) => {
return { ...older, ...state };
});
};
export const resetSchemeStatus = () => {
bookingPanelState.update((state) => ({
...state,
chosenScheme: "",
isPermutationModeSelected: false,
}));
};

12
src/lib/trpc/client.ts Executable file
View File

@@ -0,0 +1,12 @@
import type { Router } from "$lib/trpc/router";
import { createTRPCClient, type TRPCClientInit } from "trpc-sveltekit";
let browserClient: ReturnType<typeof createTRPCClient<Router>>;
export function trpc(init?: TRPCClientInit) {
const isBrowser = typeof window !== "undefined";
if (isBrowser && browserClient) return browserClient;
const client = createTRPCClient<Router>({ init });
if (isBrowser) browserClient = client;
return client;
}

17
src/lib/trpc/context.ts Executable file
View File

@@ -0,0 +1,17 @@
import { getSession } from "$lib/server/session.helpers";
import { constants } from "$lib/utils/constants";
import type { RequestEvent } from "@sveltejs/kit";
import type { inferAsyncReturnType } from "@trpc/server";
// we're not using the event parameter is this example,
// hence the eslint-disable rule
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function createContext(event: RequestEvent) {
return {
session: await getSession(
event.cookies.get(constants.SESSION_KEY_NAME) ?? ""
),
};
}
export type Context = inferAsyncReturnType<typeof createContext>;

22
src/lib/trpc/router.ts Executable file
View File

@@ -0,0 +1,22 @@
import { apiAuthRouter } from "./routers/apiauth.router";
import { apiDataRouter } from "./routers/apidata.router";
import { apiUserRouter } from "./routers/apiuser.router";
import { bookingRouter } from "./routers/booking.router";
import { drawRouter } from "./routers/draws.router";
import { presetDataRouter } from "./routers/presetdata.router";
import { postDataApiRouter } from "./routers/postdata.router";
import { sessionRouter } from "./routers/session.router";
import { createTRPCRouter } from "./t";
export const router = createTRPCRouter({
session: sessionRouter,
apiAuth: apiAuthRouter,
apiData: apiDataRouter,
apiUser: apiUserRouter,
draw: drawRouter,
presetData: presetDataRouter,
postData: postDataApiRouter,
booking: bookingRouter,
});
export type Router = typeof router;

View File

@@ -0,0 +1,102 @@
import { getSessionToken } from "$lib/server/external/api.scraping.helpers";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, protectedProcedure } from "../t";
import { constants } from "$lib/utils/constants";
import { getUUID } from "$lib/utils";
import { z } from "zod";
import { dbApiUser } from "$lib/server/db/apiuser.db";
import type { ServerError } from "$lib/utils/data.types";
import {
isSessionValidInStore,
removeSessionFromStore,
setSessionToRedis,
} from "$lib/server/utils/session.service";
export const apiAuthRouter = createTRPCRouter({
getCaptcha: protectedProcedure.mutation(async () => {
try {
const uuid = getUUID();
const res = await fetch(
`${constants.SCRAP_API_URL}/verify/image?uuid=${uuid}`,
{
headers: {
...constants.SCRAP_API_BASE_HEADERS,
Accept:
"image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
},
},
);
const bloob = await res.blob();
const imageBuffer = Buffer.from(await bloob.arrayBuffer());
const base64String = imageBuffer.toString("base64");
return { id: uuid, image: base64String };
} catch (err) {
console.log(err);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Error getting captcha image.",
});
}
}),
getNewSession: protectedProcedure
.input(
z.object({
captchaId: z.string().min(1),
captchaAnswer: z.string().min(1),
userId: z.string().optional(),
}),
)
.mutation(async ({ input }) => {
const { captchaId, captchaAnswer } = input;
let { userId, userType, password } =
await dbApiUser.getRandomDistributor();
if (input.userId) {
let _user = await dbApiUser.getUserById(input.userId);
if (!_user) {
return {
success: false,
errors: [{ message: "User not found." }],
};
}
userId = _user.userId;
userType = _user.userType;
password = _user.password;
}
const token = await getSessionToken({
code: captchaAnswer,
verifyToken: captchaId,
userId: userId,
userType: userType,
password: password,
});
console.log("[=] Token Response :: ", JSON.stringify(token, null, 2));
if (!token.ok) {
return {
success: false,
errors: [{ message: token.message }],
};
}
await setSessionToRedis(token.message, input.userId ?? "");
return { success: true, errors: [] as ServerError };
}),
isApiSessionValid: protectedProcedure
.input(
z.object({
checkingUserSession: z.boolean(),
userId: z.string().optional(),
}),
)
.query(async ({ input }) => {
return { valid: await isSessionValidInStore(input.userId) };
}),
logoutUser: protectedProcedure
.input(z.object({ userId: z.string().optional() }))
.mutation(async ({ input }) => {
const { userId } = input;
await removeSessionFromStore(userId);
return { success: true, errors: [] as ServerError };
}),
});

View File

@@ -0,0 +1,187 @@
import { redis } from "$lib/server/connectors/redis";
import { dbApiData } from "$lib/server/db/apidata.db";
import { dbDraw } from "$lib/server/db/apidraw.db";
import { dbApiUser } from "$lib/server/db/apiuser.db";
import { getData } from "$lib/server/external/api.scraping.helpers";
import { getReducedFinalSheet } from "$lib/server/finalsheet.helpers";
import { getDefaultTotals, getULID } from "$lib/utils";
import { constants } from "$lib/utils/constants";
import {
ApiUserTypes,
type APISession,
type ReducedFinalSheetData,
type ServerError,
} from "$lib/utils/data.types";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../t";
const lastFetched = {
get: async () => {
const out = await redis.get(constants.LAST_FETCHED_KEY);
if (out === null) {
return "Not fetched yet";
}
return out;
},
set: async () => {
await redis.set(constants.LAST_FETCHED_KEY, new Date().toISOString());
},
};
export const apiDataRouter = createTRPCRouter({
getDealersAndDraws: protectedProcedure.query(async () => {
const draws = await dbDraw.getAllDraws(true);
const dealers = await dbApiUser.allUsersOfType(ApiUserTypes.DEALER);
const lf = await lastFetched.get();
return { users: dealers, draws, lastFetched: lf };
}),
refetchData: protectedProcedure
.input(
z.object({
userIds: z.array(z.string()),
drawId: z.string(),
targetDate: z.string(),
}),
)
.mutation(async ({ input }) => {
const { userIds, targetDate, drawId } = input;
if (userIds.length < 1) {
return {
detail: "No users selected",
success: false,
errors: [
{ message: "No users selected to refetch data for" },
] as ServerError,
};
}
const sess = JSON.parse(
(await redis.get(constants.SCRAP_API_SESSION_KEY)) ?? "",
) as APISession;
if (sess === null) {
return {
detail: "API Session expired",
success: false,
errors: [
{
message:
"API Session expired, get a new api session and try again",
},
] as ServerError,
};
}
const userIdsInt = userIds.map((x) => parseInt(x.split(":")[1]));
const out = await getData(
sess.sessionToken,
userIdsInt,
parseInt(drawId.split(":")[1]),
targetDate,
);
if (!out.ok) {
return {
success: false,
detail: "Error fetching data",
errors: [{ message: out.message }] as ServerError,
};
}
const dataCount = out.data.length;
await dbApiData.upsertData(out.data, targetDate);
return {
detail: `Scraped ${dataCount} entries for ${userIds.length} users`,
success: true,
errors: [] as ServerError,
};
}),
getDataByFilters: protectedProcedure
.input(
z.object({ date: z.string(), drawId: z.string(), userId: z.string() }),
)
.mutation(async ({ input }) => {
const { date, drawId, userId } = input;
const data = await dbApiData.getBookingEntriesForDealer(
date,
drawId.split(":")[1],
userId.split(":")[1],
true,
);
return { data };
}),
getReducedFinalSheet: protectedProcedure
.input(z.object({ date: z.string(), drawId: z.string() }))
.mutation(async ({ input }) => {
const { date, drawId } = input;
const draw = await dbDraw.getDraw(drawId);
const fsData = {
id: getULID(),
date,
drawId,
data: [],
totals: getDefaultTotals(),
} as ReducedFinalSheetData;
if (!draw) {
return {
ok: false,
detail: `Draw for the passed draw ID not found`,
data: fsData,
errors: [
{ message: `Draw for the passed draw ID not found` },
] as ServerError,
};
}
console.log("Fetching data");
const data = await getReducedFinalSheet(fsData);
console.log(data);
if (!data.ok) {
return {
ok: false,
detail: `Error compiling final sheet`,
data: fsData,
errors: data.errors,
};
}
return {
ok: true,
detail: `Final sheet for ${date}, draw ${draw.title} has been compiled`,
data: fsData,
errors: [] as ServerError,
};
}),
getFinalSheetRow: protectedProcedure
.input(
z.object({ date: z.string(), drawId: z.string(), number: z.string() }),
)
.mutation(async ({ input }) => {
return {
ok: true,
data: {},
errors: [] as ServerError,
};
}),
delDataOlderThan2Weeks: protectedProcedure.mutation(async () => {
await dbApiData.deleteDataOlderThan2Weeks();
return { ok: true, detail: "Data older than 2 weeks has been deleted" };
}),
postTestBooking: protectedProcedure
.input(z.object({ drawId: z.string(), date: z.string() }))
.mutation(async () => {
return {
ok: true,
detail: "API not live",
errors: [] as ServerError,
};
// console.log("GENERATING TEST DATA :: ", drawId, date);
// const testData = await getTestBookingData(drawId, date);
// // console.log(testData);
// await dbApiData.upsertData(testData, date);
// return {
// ok: true,
// detail: "Test booking committed",
// errors: [] as ServerError,
// };
}),
});

Some files were not shown because too many files have changed in this diff Show More