Compare commits

..

No commits in common. "13254bb2417dca8e98fb3663e094af2a9683a92e" and "67cad656d5083940564a813f3c4ce8f782d1f3fa" have entirely different histories.

31 changed files with 107 additions and 1138 deletions

View file

@ -1,23 +0,0 @@
# WorkshopTasker
Allows for managing order tasks and inported products for a small work shop.
*A college database project*
# Running
The project was tested with the nodejs version v18.16.0. It may not work with newer versions of nodejs.
Dev dependencies are required. `npm install -D`
Project uses Oracle MySQL as a database store. It uses environment variables for the connection settigns. All provided variables below are required.
| Environment variable | Description |
|----------------------|-------------------|
| `DB_HOST` | Database host |
| `DB_PORT` | Database port |
| `DB_USER` | Database user |
| `DB_PASSWORD` | Database password |
| `DB_SCHEMA` | Database schema |
After setting variables, you can run the project using `npx nuxi dev` or `npx nuxi preview`

View file

@ -1,63 +0,0 @@
<script setup lang="ts">
import { VAlert } from 'vuetify/components';
import { computed } from 'vue';
export type AlertData = Pick<InstanceType<typeof VAlert>["$props"],
| "color"
| "icon"
| "text"
| "title"
| "type"
| "closable"
| "closeIcon"
>;
const props = defineProps<{
alerts: Array<AlertData>;
}>();
// eslint-disable-next-line func-call-spacing
const emit = defineEmits<{
(e: "update:alerts", value: typeof props.alerts): void,
}>();
const alerts = computed({
get() { return props.alerts ?? []; },
set(value: typeof props.alerts) { emit("update:alerts", value); },
});
</script>
<template>
<VOverlay
model-value
:scrim="false"
no-click-animation
persistent
class="alerts"
>
<VAlert
v-for="i of alerts"
:key="alerts.indexOf(i)"
class="alert"
:color=" i.color"
model-value
:icon="i.icon ?? false"
:text="i.text"
:title="i.title"
:type="i.type ?? 'error'"
:closable="i.closable ?? true"
:close-icon="i.closeIcon ?? undefined"
/>
</VOverlay>
</template>
<style scoped>
.alerts {
flex-direction: column-reverse;
align-items: center;
}
.alert {
margin: 0.5em;
}
</style>

View file

@ -1,51 +0,0 @@
<script setup lang="ts">
import { ref } from "vue";
type optionalMap<Optional> = Optional extends true ? undefined : string | number;
// type typeMap<Type extends string = {};
export type fieldDefinition<Optional extends boolean = boolean> = {
key: string,
label?: string,
type: "text" | "number",
optional?: Optional,
value?: optionalMap<Optional>,
}
const props = defineProps<{
fields: Array<fieldDefinition>,
modelValue?: any,
}>();
// eslint-disable-next-line func-call-spacing
const emit = defineEmits<{
(e: "update:modelValue", value: any): void,
(e: "updateSubModelValue", key: fieldDefinition["key"], value: fieldDefinition["value"]): void,
}>();
const modelValue = ref<{[key: string]: string | number | undefined}>({});
for (const i of props.fields) {
modelValue.value[i.key] = i.value;
emit("updateSubModelValue", i.key, i.value);
}
function updateModel(key: string, v: any) {
modelValue.value[key] = v;
emit("update:modelValue", modelValue.value);
emit("updateSubModelValue", key, v);
}
emit("update:modelValue", modelValue.value);
</script>
<template>
<VTextField
v-for="i of fields"
:key="i.key"
:model-value="modelValue[i.key]"
:label="i.label"
:type="i.type"
@update:model-value="v => updateModel(i.key, v)"
/>
</template>

View file

@ -13,20 +13,18 @@ const navOpen = ref(!mobile.value);
<VToolbarTitle>Database Project</VToolbarTitle>
</VAppBar>
<VNavigationDrawer v-model="navOpen" :temporary="mobile">
<!-- <VList>
<VList>
<VListItem
prepend-avatar="https://cdn.discordapp.com/avatars/317001025074757632/248f503c44bc95ac75f55f5835e6ff9f.png?size=512"
title="Anonymous"
/>
</VList> -->
</VList>
<VDivider />
<VList density="compact" nav>
<VListItem prepend-icon="mdi-login" title="Login" value="/login" @click="navigateTo('/login')" />
<VListItem prepend-icon="mdi-account" title="Clients" value="/clients" @click="navigateTo('/clients')" />
<VDivider />
<VListItem prepend-icon="mdi-home" title="Home" value="/" @click="navigateTo('/')" />
<VListItem prepend-icon="mdi-login" title="Login" value="/login" @click="navigateTo('/login')" />
<VListItem prepend-icon="mdi-table" title="Table Example" value="/tableExample" @click="navigateTo('/tableExample')" />
<VListItem prepend-icon="mdi-form-textarea" title="Echo Form" value="/forms" @click="navigateTo('/forms')" />
</VList>

View file

@ -1,56 +0,0 @@
<script setup lang="ts">
import { order as orderType } from '~/utils/types/database';
const props = defineProps<{
order?: orderType
}>();
</script>
<template>
<!-- <VRow v-if="props.order">
{{ props.order.id }}
</VRow> -->
<VCol>
<div v-if="props.order && props.order.work.length !== 0">
<span class="text-h4">Works</span>
<VTable class="noScroll">
<thead>
<tr>
<th>fulfilled</th>
<th>offer</th>
<th>price</th>
</tr>
</thead>
<tbody>
<tr v-for="i in props.order.work" :key="i.id">
<td>{{ i.is_fulfilled }}</td>
<td>
{{ i.offer.name }}
</td>
<td>{{ i.price }} PLN</td>
</tr>
</tbody>
</VTable>
</div>
<div v-if="props.order && props.order.imported_products.length !== 0">
<span class="text-h4">Imported products</span>
<VTable>
<thead>
<tr>
<th>Name</th>
<th>link</th>
<th>price</th>
</tr>
</thead>
<tbody>
<tr v-for="i in props.order.imported_products" :key="i.id">
<td>{{ i.name }}</td>
<td>{{ i.link }}</td>
<td>{{ i.price }} PLN</td>
</tr>
</tbody>
</VTable>
</div>
</VCol>
</template>

View file

@ -2,7 +2,6 @@
const props = defineProps<{
records: Array<any>,
recordKey: string,
recordValue?: string,
variant?: "default" | "inset" | "accordion" | "popout",
modelValue?: any,
}>();
@ -23,7 +22,6 @@ defineEmits<{
v-for="record in records"
:key="record[recordKey]"
:variant="props.variant ?? 'default'"
:value="recordValue !== undefined ? record[recordValue] : undefined"
>
<template #title>
<slot name="title" :record="record" />

View file

@ -53,8 +53,4 @@ defineEmits<{
flex-direction: row;
flex-wrap: nowrap;
}
tr:hover {
background-color: rgba(var(--v-theme-on-background), calc(var(--v-hover-opacity) * var(--v-theme-overlay-multiplier)));
}
</style>

View file

@ -1,14 +1,9 @@
<script setup lang="ts">
/* global $fetch */
import { useRoute, useFetch, createError } from "nuxt/app";
import { ref, Ref } from "vue";
import { Ref } from "vue";
import { VBtn, VForm } from "vuetify/components";
import PagedList from "~/components/pagedList.vue";
import Snowflake from "~/utils/snowflake";
import { client as clientType, order, orderSummary } from "~/utils/types/database";
import OrderView from "~/components/orderView.vue";
import EntryEditor, { fieldDefinition } from "~/components/entryEditor.vue";
import { client as clientType } from "~/utils/types/database";
const route = useRoute();
const id = route.params.id;
@ -16,116 +11,11 @@ const id = route.params.id;
const clientRequest = await useFetch(`/api/clients/${id}`);
if (clientRequest.error.value) throw createError(clientRequest.error.value?.data ?? "");
const client = clientRequest.data as Ref<clientType>;
console.log(client);
const clientOrdersRequest = await useFetch(`/api/clients/${id}/orders`);
if (clientOrdersRequest.error.value) throw createError(clientOrdersRequest.error.value?.data ?? "");
const clientOrders = clientOrdersRequest.data as Ref<Array<orderSummary>>;
const orders = ref<Map<string, {
loading: boolean,
value?: order
}>>(new Map());
for (const i of clientOrders.value)
orders.value.set(i.id, { loading: false });
async function loadOrder(id: string) {
const entry = orders.value.get(id);
if (!entry) throw createError(`excepted order entry for ${id}`);
entry.loading = true;
// @ts-expect-error
entry.value = await $fetch(`/api/orders/${id}` as "/api/order/:id", {
method: "GET",
});
entry.loading = false;
}
let lastPagedListVModel: Array<string> = [];
function updatePagedListVModel(element: Array<string>) {
const justOpened = element.filter(e => !lastPagedListVModel.includes(e));
for (const i of justOpened) loadOrder(i);
lastPagedListVModel = element;
}
const editMode = ref<boolean>(route.query?.edit === "1");
function editorFields(): Array<fieldDefinition> {
return [
{ key: "name", type: "text", label: "Name", value: client.value.name ?? undefined },
{ key: "address", type: "text", label: "Address", value: client.value.address ?? undefined },
{ key: "phone", type: "text", label: "Phone", value: client.value.phone ?? undefined },
{ key: "email", type: "text", label: "E-mail", value: client.value.email ?? undefined },
];
}
const submitting = ref<boolean>(false);
// const updateForm = ref<VForm | null>(null);
const formButton = ref<VBtn | null>(null);
const formData = ref<any>({});
function normalizeForm() {
for (const i in formData.value)
formData.value[i] = formData.value[i] === "" ? null : formData.value[i];
}
async function handleSubmit() {
submitting.value = true;
normalizeForm();
try {
const result = await $fetch(
`/api/clients/${client.value.id}` as "/api/clients/:id", {
method: "PATCH",
body: formData.value,
},
);
client.value = result;
} catch (e) {
console.error(e);
submitting.value = false;
return;
}
submitting.value = false;
editMode.value = false;
}
function getCreationDate() {
const date = new Date(Number(new Snowflake(BigInt(client.value.id)).timestamp.toString())).toLocaleDateString();
return date;
}
</script>
<template>
<VDialog
v-model="editMode"
:persistent="submitting"
:activator="formButton as unknown as (Element | null) ?? undefined"
width="auto"
>
<VCard width="400px" :loading="submitting">
<VCardTitle>
Edit client
</VCardTitle>
<VForm
ref="updateForm"
:disabled="submitting"
class="px-4"
>
<EntryEditor
:fields="editorFields()"
@update-sub-model-value="(k, v) => { formData[k] = v; }"
/>
</VForm>
<VCardActions>
<VBtn
color="primary"
@click="handleSubmit"
>
Submit
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<VRow>
<VCol cols="12">
<div
@ -138,7 +28,7 @@ function getCreationDate() {
</VRow>
<VRow>
<VCol md="4" cols="12">
<VCard class="mx-auto">
<VCard>
<VList>
<VListItem
v-if="client.address"
@ -148,14 +38,6 @@ function getCreationDate() {
{{ client.address }}
</VListItemTitle>
</VListItem>
<VListItem
v-if="client.phone"
prepend-icon="mdi-phone"
>
<VListItemTitle class="text-wrap">
{{ client.phone }}
</VListItemTitle>
</VListItem>
<VListItem
v-if="client.email"
prepend-icon="mdi-email"
@ -164,44 +46,27 @@ function getCreationDate() {
{{ client.email }}
</VListItemTitle>
</VListItem>
</VList>
<template #actions>
<VBtn
ref="formButton"
<VListItem
v-if="client.phone"
prepend-icon="mdi-phone"
>
edit
</VBtn>
</template>
<VListItemTitle class="text-wrap">
{{ client.phone }}
</VListItemTitle>
</VListItem>
</VList>
</VCard>
<span class="font-italic text-caption">Created {{ getCreationDate() }}</span>
</VCol>
<VCol cols="12" md="8">
<PagedList
:records="clientOrders"
record-key="id"
record-value="id"
@update:model-value="updatePagedListVModel"
:records="[{a: 'owo'}, {a: 'uwu'}, {a: 'qwq'}]"
record-key="a"
>
<template #title="i">
<VRow>
<VCol>{{ new Date(Number(new Snowflake(BigInt(((i.record) as orderSummary).id)).timestamp)).toLocaleDateString() }}</VCol>
<VCol>{{ ((i.record) as orderSummary).value }} PLN</VCol>
<VCol>
{{ ((i.record) as orderSummary).imported_products_count }}
products,
{{ ((i.record) as orderSummary).work_count }}
works
</VCol>
</VRow>
</template>
<template #text="i">
<VProgressLinear
:height="orders.get((i.record as orderSummary).id)?.loading ?? true ? undefined : 0"
absolute
:progress="orders.get((i.record as orderSummary).id)?.loading ?? true"
:indeterminate="orders.get((i.record as orderSummary).id)?.loading ?? true"
/>
<OrderView :order="(orders.get((i.record as orderSummary).id)?.value as order | undefined)" />
{{ i }}
</template>
<template #title="i">
{{ i }}
</template>
</PagedList>
</VCol>

View file

@ -1,19 +1,12 @@
<script setup lang="ts">
/* global $fetch */
import { useFetch, createError, navigateTo, NuxtError, useRoute } from "nuxt/app";
import { ref, Ref, reactive } from "vue";
import { VBtn } from "vuetify/components";
import { useFetch, createError } from "nuxt/app";
import { ref, Ref } from "vue";
import { definePageMeta } from "~/.nuxt/imports";
import { client as clientType } from "~/utils/types/database";
import pagedTable from "~/components/pagedTable.vue";
import Alerts, { AlertData } from "~/components/alerts.vue";
import { fieldDefinition } from "~/components/entryEditor.vue";
definePageMeta({ middleware: ["auth"] });
const route = useRoute();
const alerts = ref<Array<AlertData>>([]);
const clientsRequest = await useFetch("/api/clients");
if (clientsRequest.error.value) throw createError(clientsRequest.error.value?.data ?? "");
@ -23,11 +16,8 @@ const countRequest = await useFetch("/api/clients/count");
if (countRequest.error.value) throw createError(countRequest.error.value?.data ?? "");
const count = countRequest.data as Ref<NonNullable<typeof countRequest.data.value>>;
async function rowClicked(client: string, edit = false) {
await navigateTo({
path: `/client/${client}`,
query: { edit: edit ? 1 : undefined },
});
function rowClicked(client: string, edit = false) {
console.log(client);
}
async function rowDelete(client: string) {
@ -38,7 +28,7 @@ async function rowDelete(client: string) {
clients.value = clients.value.filter(e => e.id !== client);
count.value.count--;
} catch (e) {
alerts.value.push({ text: (e as NuxtError).message, type: "error" });
// FIXME: show the error
console.log(e);
}
}
@ -47,98 +37,28 @@ const loadingMore = ref<boolean>(false);
async function loadBefore() {
loadingMore.value = true;
try {
clients.value.push(...await $fetch("/api/clients", {
query: {
before: clients.value[clients.value.length - 1].id,
},
}));
} catch (e) {
alerts.value.push({ text: (e as NuxtError).message });
console.error(e);
}
loadingMore.value = false;
}
const createMode = ref<boolean>(route.query?.create === "1");
function editorFields(): Array<fieldDefinition> {
return [
{ key: "name", type: "text", label: "Name" },
{ key: "address", type: "text", label: "Address" },
{ key: "phone", type: "text", label: "Phone" },
{ key: "email", type: "text", label: "E-mail" },
];
}
const submitting = ref<boolean>(false);
// const updateForm = ref<VForm | null>(null);
const formButton = ref<VBtn | null>(null);
const formData = ref<any>({
name: null,
address: null,
phone: null,
email: null,
});
function normalizeForm() {
for (const i in formData.value)
formData.value[i] = formData.value[i] === "" ? null : formData.value[i];
}
async function handleSubmit() {
submitting.value = true;
normalizeForm();
try {
const result = await $fetch(
"/api/clients/", {
method: "POST",
body: formData.value,
},
);
} catch (e) {
console.error(e);
submitting.value = false;
return;
}
submitting.value = false;
createMode.value = false;
}
</script>
<template>
<Alerts :alerts="alerts" />
<VDialog
v-model="createMode"
:persistent="submitting"
:activator="formButton as unknown as (Element | null) ?? undefined"
width="auto"
<VOverlay
model-value
origin="top center"
:scrim="false"
height="fit-content"
persistent
no-click-animation
>
<VCard width="400px" :loading="submitting">
<VCardTitle>
Create client
</VCardTitle>
<VForm
ref="updateForm"
:disabled="submitting"
class="px-4"
>
<EntryEditor
:fields="editorFields()"
@update-sub-model-value="(k, v) => { formData[k] = v; }"
/>
</VForm>
<VCardActions>
<VBtn
color="primary"
@click="handleSubmit"
>
Submit
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<VAlert class="alert">
owowowowowowowowo
</VAlert>
</VOverlay>
<VRow>
<VCol>
<VBreadcrumbs :items="['Clients']" />
@ -146,11 +66,6 @@ async function handleSubmit() {
<div class="text-h4">
There are {{ count?.count }} clients in the database.
</div>
<VBtn
ref="formButton"
>
Create
</VBtn>
</VCol>
</VRow>
<VRow>

View file

@ -3,8 +3,7 @@ import Test from '~/components/test.vue';
</script>
<template>
<h1>Hi mom!</h1>
<br>
<h1>Hi mom!</h1><br>
<Test />
<Test />
<Test />

View file

@ -124,13 +124,6 @@ updateUserInfo();
>
Logout
</VBtn>
<VBtn
v-if="loggedIn"
color="primary"
@click="navigateTo('/clients')"
>
go to clients
</VBtn>
</template>
</VCard>
</template>

Binary file not shown.

View file

@ -1,9 +0,0 @@
/* global defineEventHandler */
import { baaWrapper } from "~/server/api/orders.get";
export default defineEventHandler(async (e) => {
const baa = await baaWrapper.RESTget(e, 50, 200, "`client` = ?", [e.context.params?.id]);
console.log(baa);
return baa;
});

View file

@ -3,8 +3,8 @@ import crypto from "crypto";
import { database, data } from "../utils/database";
import { isString } from "../utils/isString";
import Snowflake from "../utils/snowflake";
import { cookieSettings } from "../utils/rootUtils";
import Snowflake from "~/utils/snowflake";
export default defineEventHandler(async (e) => {
if (getCookie(e, "token"))

View file

@ -1,37 +0,0 @@
/* global defineEventHandler */
import BaaPagination from "../utils/baaPagination";
import { data, database } from "../utils/database";
import { client, orderSummary } from "~/utils/types/database";
export const baaWrapper = new BaaPagination<orderSummary, "id">(
"orderSummaries",
"id",
"*, CONVERT(`client`, CHAR) AS `client`, CONVERT(`user`, CHAR) as `user`",
);
export default defineEventHandler(async (e) => {
const orders = await baaWrapper.RESTget(e, 50, 200);
const uniqueClients: Array<string> = [];
for (const i of orders) {
if (!uniqueClients.includes(i.client))
uniqueClients.push(database.escape(i.client));
}
const [clients] = await database.query(
["SELECT",
"*,",
"CONVERT(`id`, CHAR) AS `id`",
"FROM `clients`",
"WHERE `id` IN",
`(${uniqueClients.join(', ')})`,
].join(" "),
) as data<client>;
const rvalue: Array<Omit<typeof orders, "client"> | { client?: client }> = [];
for (const i of orders)
rvalue.push({ ...i, client: clients.find(e => i.client === e.id) });
return rvalue;
});

View file

@ -1,172 +0,0 @@
/* global defineEventHandler, createError, readBody, setResponseStatus */
import { createValidationError, handleRecursedValidationError } from "../utils/validation";
import { database as db } from "../utils/database";
import getRequestingUser from "../utils/getRequestingUser";
import { getOrder } from "./orders/[id].get";
import Snowflake from "~/utils/snowflake";
type importedProduct = {
name: string | null,
link: string,
price_imported: number,
price: number,
}
type work = {
offer: string,
price: number,
notes: string | null,
is_fulfilled: boolean | 0 | 1,
}
type order = {
client: string,
// user: string,
is_draft: boolean | 0 | 1,
imported_products: Array<importedProduct>,
work: Array<work>,
};
export function checkIsWork<Patch extends boolean = boolean>(
value: any,
patch: Patch,
): value is Patch extends true ? Partial<work> : work {
const errors = new Map<string, string>();
if (typeof value !== "object") {
throw createError({
message: "Invalid body",
statusCode: 400,
});
}
if (!(typeof value.offer === "string" || (patch && value.offer === undefined))) errors.set("offer", "is not string");
if (!(typeof value.price === "number" || (patch && value.price === undefined))) errors.set("price", "is not price");
if (!(typeof value.notes === "string" || value.notes === null || (patch && value.notes === undefined))) errors.set("notes", "is not string or null");
if (!(typeof value.is_fulfilled === "boolean" || value.is_fulfilled === 0 || value.is_fulfilled === 1 || (patch && value.is_fulfilled === undefined))) errors.set("is_fulfilled", "is not boolean");
if (errors.size !== 0) throw createValidationError(errors);
return true;
}
export function checkIsImportedProduct<Patch extends boolean = boolean>(
value: any,
patch: Patch,
): value is Patch extends true ? Partial<importedProduct> : importedProduct {
const errors = new Map<string, string>();
if (typeof value !== "object") {
throw createError({
message: "Invalid body",
statusCode: 400,
});
}
if (!(typeof value.name === "string" || value.name === null || (patch && value.name === undefined))) errors.set("name", "is not string or null");
if (!(typeof value.link === "string" || (patch && value.name === undefined))) errors.set("link", "is not string");
if (!(typeof value.price_imported === "number" || (patch && value.name === undefined))) errors.set("price_imported", "is not number");
if (!(typeof value.price || (patch && value.price === undefined))) errors.set("price", "is not number");
if (errors.size !== 0) throw createValidationError(errors);
return true;
}
export function checkIsOrder<Patch extends boolean = boolean>(
value: any,
patch: Patch,
): value is Patch extends true ? Partial<Pick<order, "client" | "is_draft">> : order {
const errors = new Map<string, string>();
if (typeof value !== "object") {
throw createError({
message: "Invalid body",
statusCode: 400,
});
}
if (!(typeof value.client === "string" || (patch && value.client === undefined))) errors.set("client", "is not string");
if (!(typeof value.is_draft === "boolean" || value.is_draft === 0 || value.is_draft === 1 || (patch && value.is_draft === undefined))) errors.set("is_draft", "is not boolean");
if (!(value.imported_products instanceof Array)) errors.set("imported_products", "is not array");
else if (patch && value.imported_products !== undefined) errors.set("imported_products", "cannot patch from order");
if (!(value.work instanceof Array)) errors.set("work", "is not array");
else if (patch && value.work !== undefined) errors.set("work", "cannot patch from order");
if (!patch) {
const importedProducts = value.imported_products;
if (importedProducts instanceof Array) {
for (const i in importedProducts) {
try {
checkIsImportedProduct(importedProducts[i], patch);
} catch (e) {
handleRecursedValidationError(e, errors, `imported_products[${i}]`);
}
}
}
const work = value.work;
if (work instanceof Array) {
for (const i in work) {
try {
checkIsWork(work[i], patch);
} catch (e) {
handleRecursedValidationError(e, errors, `work[${i}]`);
}
}
}
}
if (errors.size !== 0) throw createValidationError(errors);
return true;
}
export default defineEventHandler(async (e) => {
const body = await readBody(e);
const id = new Snowflake().toString();
const user = await getRequestingUser(e);
if (!checkIsOrder(body, false)) throw createError({ message: "Invalid body", statusCode: 400 });
const database = await db.new();
await database.beginTransaction();
await database.query(
["INSERT INTO",
"`orders`",
"VALUES",
"(?, ?, ?, ?)",
].join(" "),
[id, body.client, user.id, body.is_draft],
);
const promises: Array<Promise<any>> = [];
for (const i of body.imported_products) {
promises.push(database.query(
["INSERT INTO",
"`imported_products`",
"VALUES",
"(?, ?, ?, ?, ?, ?)",
].join(" "),
[new Snowflake().toString(), id, i.name, i.link, i.price_imported, i.price],
));
}
for (const i of body.work) {
promises.push(database.query(
["INSERT INTO",
"`work`",
"VALUES",
"(?, ?, ?, ?, ?, ?)",
].join(" "),
[new Snowflake().toString(), id, i.offer, i.price, i.notes, i.is_fulfilled],
));
}
await Promise.all(promises);
await database.commit();
setResponseStatus(e, 201);
return getOrder(id);
});

View file

@ -1,17 +0,0 @@
/* global defineEventHandler, createError */
import { ResultSetHeader } from "mysql2";
import { database } from "~/server/utils/database";
export default defineEventHandler(async (e) => {
const id = e.context.params?.id;
const [result] = await database.query(
"DELETE FROM `orders` WHERE `id` = ?",
[id],
) as unknown as [ResultSetHeader];
if (result.affectedRows === 0) throw createError({ statusCode: 404 });
return null;
});

View file

@ -1,108 +0,0 @@
/* global defineEventHandler, createError */
import { offer as offerType, order } from "~/utils/types/database";
import { database, data } from "~/server/utils/database";
export async function orderExists(id: string) {
const [[exists]] = await database.query(
"SELECT EXISTS(*) AS `exists` FROM `orders` WHERE `id` = ?",
[id],
) as data<{exists: 0 | 1}>;
return exists.exists === 1;
}
export async function getImportedProducts(id: string) {
const [importedProducts] = await database.query(
["SELECT",
"CONVERT(`id`, CHAR) AS `id`,",
"`name`,",
"`link`,",
"`price`,",
"`price_imported`",
"FROM `imported_products`",
"WHERE `order` = ?",
].join(" "),
[id],
) as data<{
id: string,
name: string | null,
link: string,
price: string,
price_imported: string
}>;
return importedProducts;
}
export async function getWork(id: string) {
const [work] = await database.query(
["SELECT",
"CONVERT(`id`, CHAR) AS `id`,",
"CONVERT(`offer`, CHAR) AS `offer`,",
"`price`,",
"`notes`,",
"`is_fulfilled`",
"FROM `work`",
"WHERE `order` = ?",
].join(" "),
[id],
) as data<{
id: string,
offer: offerType,
price: number,
notes: string | null,
is_fulfilled: 0 | 1,
}>;
const [offer] = await database.query(
["SELECT",
"CONVERT(`offer`.`id`, CHAR) AS `id`,",
"`offer`.`name`,",
"`offer`.`description`,",
"`offer`.`recommended_price`",
"FROM",
"`work`",
"LEFT JOIN `offer` ON `work`.`offer` = `offer`.`id`",
"WHERE `work`.`order` = ?",
].join(" "),
[id],
) as data<offerType>;
// @ts-ignore i.offer is string, but it needs to be an offer object
for (const i of work) i.offer = offer.find(e => e.id === i.offer) as offerType;
return work;
}
export async function getOrder(id: string): Promise<order> {
const [[order]] = await database.query(
["SELECT",
"CONVERT(`id`, CHAR) AS `id`,",
"CONVERT(`client`, CHAR) AS `client`,",
"CONVERT(`user`, CHAR) AS `user`, ",
"`is_draft`,",
"`value`",
"FROM `orderSummaries`",
"WHERE `id` = ?",
].join(" "),
[id],
) as data<{
id: string,
client: string,
user: string,
is_draft: 0 | 1,
value: number,
}>;
if (!order) throw createError({ statusCode: 404 });
const importedProducts = await getImportedProducts(id);
const work = await getWork(id);
return { ...order, imported_products: importedProducts, work };
}
export default defineEventHandler((e) => {
const key = e.context.params?.id;
return getOrder(key as string);
});

View file

@ -1,17 +0,0 @@
/* global defineEventHandler, readBody, createError */
import { checkIsOrder } from "../orders.post";
import { database as db } from "~/server/utils/database";
export default defineEventHandler(async (e) => {
const body = await readBody(e);
const id = e.context.params?.id;
if (!checkIsOrder(e, true)) throw createError({ message: "Invalid body", statusCode: 400 });
const database = await db.new();
await database.beginTransaction();
for (const [k, v] of Object.entries(body))
database.query(`UPDATE TABLE \`orders\` SET \`${k}\` = ? WHERE \`id\` = ?`, [v, id]);
});

View file

@ -1,12 +0,0 @@
/* global defineEventHandler, createError */
import { orderExists, getImportedProducts } from "../[id].get";
export default defineEventHandler(async (e) => {
const id = e.context.params?.id as string;
if (!orderExists(id)) throw createError({ statusCode: 404 });
const importedProducts = await getImportedProducts(id);
return importedProducts;
});

View file

@ -1,27 +0,0 @@
/* global defineEventHandler, readBody, createError, setResponseStatus */
import { checkIsImportedProduct } from "../../orders.post";
import { getImportedProducts, orderExists } from "../[id].get";
import Snowflake from "~/utils/snowflake";
import { database } from "~/server/utils/database";
export default defineEventHandler(async (e) => {
const body = await readBody(e);
const idOrder = e.context.params?.id as string;
const idImportedProducts = new Snowflake().toString();
if (!orderExists(idOrder)) throw createError({ statusCode: 404 });
if (!checkIsImportedProduct(body, false)) throw createError({ message: "Invalid body", statusCode: 400 });
await database.query(
["INSERT INTO",
"`imported_products`",
"VALUES",
"(?, ?, ?, ?, ?, ?)",
].join(" "),
[idImportedProducts, idOrder, body.name, body.link, body.price_imported, body.price],
);
setResponseStatus(e, 201);
return getImportedProducts(idOrder);
});

View file

@ -1,12 +0,0 @@
/* global defineEventHandler, createError */
import { orderExists, getWork } from "../[id].get";
export default defineEventHandler(async (e) => {
const id = e.context.params?.id as string;
if (!orderExists(id)) throw createError({ statusCode: 404 });
const work = await getWork(id);
return work;
});

View file

@ -1,28 +0,0 @@
/* global defineEventHandler, readBody, createError, setResponseStatus */
import { checkIsWork } from "../../orders.post";
import { getWork, orderExists } from "../[id].get";
import Snowflake from "~/utils/snowflake";
import { database } from "~/server/utils/database";
export default defineEventHandler(async (e) => {
const body = await readBody(e);
const idOrder = e.context.params?.id as string;
const idWork = new Snowflake().toString();
if (!orderExists(idOrder)) throw createError({ statusCode: 404 });
if (!checkIsWork(body, false)) throw createError({ message: "Invalid body", statusCode: 400 });
await database.query(
["INSERT INTO",
"`work`",
"VALUES",
"(?, ?, ?, ?, ?, ?)",
].join(" "),
[idWork, idOrder, body.offer, body.price, body.notes, body.is_fulfilled],
);
setResponseStatus(e, 201);
return getWork(idWork);
});

View file

@ -1,20 +0,0 @@
/* global defineEventHandler, createError */
import { ResultSetHeader } from "mysql2";
import { orderExists } from "../../[id].get";
import { database } from "~/server/utils/database";
export default defineEventHandler(async (e) => {
const idOrder = e.context.params?.id as string;
const idWork = e.context.params?.idWork as string;
if (!orderExists(idOrder)) throw createError({ statusCode: 404 });
const [response] = await database.query(
"DELETE FROM `work` WHERE `id` = ?",
[idWork],
) as unknown as [ResultSetHeader];
if (response.affectedRows === 0) throw createError({ statusCode: 404 });
return null;
});

View file

@ -1,11 +0,0 @@
/* global defineEventHandler, createError */
import { orderExists, getWork } from "../../[id].get";
export default defineEventHandler((e) => {
const idOrder = e.context.params?.id as string;
const idWork = e.context.params?.idWork as string;
if (!orderExists(idOrder)) throw createError({ statusCode: 404 });
return getWork(idWork);
});

View file

@ -1,11 +1,11 @@
/* global defineEventHandler, getQuery, createError, readBody, setResponseStatus */
/* global defineEventHandler, getQuery, createError, readBody */
import { QueryObject } from "ufo";
import { H3Event } from "h3";
import { ResultSetHeader } from "mysql2/promise";
import { data, database } from "./database";
import { isString } from "./isString";
import Snowflake from "~/utils/snowflake";
import Snowflake from "./snowflake";
import { client } from "~/utils/types/database";
type queryType = {
@ -21,12 +21,6 @@ type queryType = {
export default class BaaPagination<T extends {[k: string]: any}, keyType extends string = "id"> {
readonly table: string;
readonly key: keyType;
readonly select: string;
readonly groupBy: string;
private get sqlGroupBy() {
return this.groupBy !== "" ? `GROUP BY ${this.groupBy}` : "";
}
/**
* Gets queryType for a given query with a value
@ -69,40 +63,36 @@ export default class BaaPagination<T extends {[k: string]: any}, keyType extends
async getPagedResults(
queryType: queryType,
limit = 50,
where = "",
bind: Array<any> = [],
) {
const sqlwhere = where !== "" ? `AND (${where})` : "";
switch (queryType.type) {
case "before": {
const [data] = await database.query(
`SELECT ${this.select}, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` < ? ${sqlwhere} ORDER BY \`${this.key}\` DESC ${this.sqlGroupBy} LIMIT ?`,
[queryType.id, ...bind, limit],
`SELECT *, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` < ? ORDER BY \`${this.key}\` DESC LIMIT ?`,
[queryType.id, limit],
) as unknown as data<T>;
return data;
}
case "after": {
const [data] = await database.query(
`SELECT ${this.select}, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` > ? ${sqlwhere} ORDER BY \`${this.key}\` DESC ${this.sqlGroupBy} LIMIT ?`,
[queryType.id, ...bind, limit],
`SELECT *, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`$this.key\` > ? ORDER BY \`${this.key}\` DESC LIMIT ?`,
[queryType.id, limit],
) as unknown as data<T>;
return data;
}
case "around": {
const [data] = await database.query(
` SELECT ${this.select}, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM (\n` +
`(SELECT * FROM \`${this.table}\` WHERE \`${this.key}\` >= ? ${sqlwhere} ORDER BY \`${this.key}\` ${this.sqlGroupBy} ASC LIMIT ?)\n` +
`(SELECT *, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` >= ? ORDER BY \`${this.key}\` ASC LIMIT ?)\n` +
"UNION ALL\n" +
`(SELECT ${this.select} FROM \`${this.table}\` WHERE \`${this.key}\` < ? ${sqlwhere} ORDER BY \`${this.key}\` DESC ${this.sqlGroupBy} LIMIT ?)\n` +
`) as \`x\` ORDER BY \`${this.key}\` DESC`,
[queryType.id, ...bind, Math.ceil(limit / 2), queryType.id, ...bind, Math.floor(limit / 2)],
`(SELECT *, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` < ? ORDER BY \`${this.key}\` DESC LIMIT ?)\n` +
"ORDER BY `id` DESC",
[queryType.id, Math.ceil(limit / 2), queryType.id, Math.floor(limit / 2)],
) as unknown as data<T>;
return data;
}
case null: {
const [data] = await database.query(
`SELECT ${this.select}, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE TRUE ${sqlwhere} ORDER BY \`${this.key}\` DESC ${this.sqlGroupBy} LIMIT ?`,
[...bind, limit],
`SELECT *, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` ORDER BY \`${this.key}\` DESC LIMIT ?`,
[limit],
) as unknown as data<T>;
return data;
}
@ -111,13 +101,7 @@ export default class BaaPagination<T extends {[k: string]: any}, keyType extends
}
}
RESTget(
e: H3Event,
defaultLimit = 50,
limitLimit = 200,
where = "",
bind: Array<any> = [],
) {
RESTget(e: H3Event, defaultLimit = 50, limitLimit = 200) {
const query = getQuery(e);
let limit = defaultLimit;
@ -137,7 +121,7 @@ export default class BaaPagination<T extends {[k: string]: any}, keyType extends
const queryData = BaaPagination.getLocationParameterType(query);
return this.getPagedResults(queryData, limit, where, bind);
return this.getPagedResults(queryData, limit);
}
async RESTpost<K extends keyof Omit<T, keyType>>(
@ -154,7 +138,7 @@ export default class BaaPagination<T extends {[k: string]: any}, keyType extends
arrayToInsert.push(...fields.map(field => body[field]));
await database.query(
`INSERT INTO \`${this.table}\` ` +
"INSERT INTO `clients` " +
`(\`${this.key}\`,\`${fields.join("`, `")}\`) ` +
"VALUES (" +
"?, ".repeat(fields.length) +
@ -162,8 +146,6 @@ export default class BaaPagination<T extends {[k: string]: any}, keyType extends
arrayToInsert,
);
setResponseStatus(e, 201);
// FIXME: data may be turncated in the database
// either throw an error when data is too large or
// reply with turncated data
@ -173,7 +155,7 @@ export default class BaaPagination<T extends {[k: string]: any}, keyType extends
async RESTgetRecord(e: H3Event) {
const key = e.context.params?.[this.key];
const [data] = await database.query(
`SELECT ${this.select}, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` = ?`,
`SELECT *, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` = ?`,
[key],
) as data<T>;
@ -212,7 +194,7 @@ export default class BaaPagination<T extends {[k: string]: any}, keyType extends
}
const [data] = await database.query(
`SELECT ${this.select}, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` = ?`,
`SELECT *, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` = ?`,
[key],
) as data<T>;
@ -232,30 +214,17 @@ export default class BaaPagination<T extends {[k: string]: any}, keyType extends
return null;
}
async RESTrecordCount(
e :H3Event,
where = "",
bind: Array<any> = [],
) {
const sqlwhere = where !== "" ? `WHERE ${where}` : "";
async RESTrecordCount(e :H3Event) {
const [[data]] = await database.query(
`SELECT COUNT(*) as \`count\` FROM \`${this.table}\` ${sqlwhere} ${this.sqlGroupBy}`,
bind,
`SELECT COUNT(*) as \`count\` FROM \`${this.table}\``,
) as data<{count: number}>;
if (!data) throw createError("Database returned no rows");
return data;
}
constructor(
table: string,
key: keyType,
select = "*",
groupBy = "",
) {
constructor(table: string, key: keyType) {
this.table = table;
this.key = key;
this.select = select;
this.groupBy = groupBy;
}
}

View file

@ -1,17 +1,11 @@
import mysql, { Connection } from "mysql2/promise";
import mysql from "mysql2/promise";
const connectionOptions: mysql.ConnectionOptions = {
export const database = await mysql.createConnection({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_SCHEMA,
decimalNumbers: true,
supportBigNumbers: true,
};
export const database =
await mysql.createConnection(connectionOptions) as Connection & { new: () => Promise<Connection> };
database.new = () => { return mysql.createConnection(connectionOptions); };
});
export type data<T> = [T[], mysql.FieldPacket[]];

View file

@ -1,25 +0,0 @@
/* global getCookie, createError */
import { H3Event } from "h3";
import { database, data } from "./database";
import { user } from "~/utils/types/database";
export default async function getRequestingUser(e: H3Event) {
const cookie = getCookie(e, "token");
const [[user]] = await database.query(
["SELECT",
"CONVERT(`users`.`id`, CHAR) as `id`,",
"`users`.`username`,",
"`users`.`email`,",
"`users`.`display_name`",
"FROM",
"`sessions`",
"LEFT JOIN `users` ON `sessions`.`user` = `users`.`id`",
"WHERE `sessions`.`id` = ?",
].join(" "),
[cookie],
) as data<user>;
if (!user) throw createError("User not found");
return user;
}

View file

@ -31,11 +31,7 @@ export default class Snowflake {
this.state = state + (value << 0n);
}
constructor(value?: bigint) {
if (value) {
this.state = BigInt.asUintN(64, value);
return;
}
constructor() {
this.set_timestamp(Date.now());
this.set_processid(1);
this.set_increment(Snowflake.increment());
@ -44,8 +40,4 @@ export default class Snowflake {
public toString() {
return this.state.toString();
}
public get timestamp() {
return BigInt.asUintN(64 - 22, this.state >> 22n);
}
}

View file

@ -1,31 +0,0 @@
/* global createError */
export function createValidationError(errors: Map<string, string>) {
let message = "Invalid parameters: ";
for (const i in errors)
message += i + ", ";
message = message.slice(0, -2);
return createError({
statusCode: 400,
message,
data: {
errors: Object.fromEntries(errors),
},
});
}
export function handleRecursedValidationError(e: unknown, errors: Map<string, string>, element: string) {
if (typeof e !== "object") throw e;
if (!e) throw e;
if (!(e as any).data || !(e as any).message) throw e;
const upstreamErrors = (e as any).data.errors as any;
const upstreamMessage = (e as any).message;
if (upstreamErrors) {
for (const j in upstreamErrors)
errors.set(`${element}.${j}`, String(upstreamErrors[j]));
} else if (upstreamMessage) {
errors.set(`${element}`, String(upstreamMessage));
} else {
throw e;
}
}

View file

@ -1,99 +1,68 @@
export interface client {
id: string;
name: string | null;
address: string | null;
phone: string | null;
email: string | null;
id: string,
name: string | null,
address: string | null,
phone: string | null,
email: string | null,
}
export interface user {
id: string;
username: string;
email: string;
display_name?: string;
id: string,
username: string,
email: string,
display_name?: string,
}
export interface session {
id: string;
user: string;
expiry_date: string;
id: string,
user: string,
expiry_date: string,
}
export interface imported_product {
id: string;
id: string,
// order: string,
name?: string;
link: string;
price_imported: string;
price: string;
name?: string,
link: string,
price_imported: string,
price: string,
}
export interface offer {
id: string;
name: string;
description?: string;
recommended_price?: string;
id: string,
name: string,
description?: string,
recommended_price?: string,
}
export interface work {
id: string;
id: string,
// order: string,
offer: string | offer;
price: string;
notes: string;
is_fulfilled: boolean;
offer: string|offer,
price: string,
notes: string,
is_fulfilled: boolean,
}
export interface order {
imported_products: Array<{
id: string;
name: string | null;
link: string;
price: string;
price_imported: string;
}>;
work: {
id: string;
offer: offer;
price: number;
notes: string | null;
is_fulfilled: 0 | 1;
}[];
id: string;
client: string;
user: string;
is_draft: 0 | 1;
value: number;
}
export interface orderSummary {
id: string;
client: string;
user: string;
is_draft: 0 | 1;
value: string;
imported_products_count: number;
work_count: number;
id: string,
client: client|string,
user: user|string,
is_draft: boolean,
imported_products: imported_product[],
work: work[],
}
export interface work_template {
id: string;
id: string,
// order_template: string,
offer: string | offer;
price: string;
notes?: string;
offer: string|offer,
price: string,
notes?: string,
}
export interface order_template {
id: string;
name: string;
description?: string;
id: string,
name: string,
description?: string,
}
// 1 is true, 0 is false
export type Dboolean =
| boolean
| 0 // false
| 1; // true
export type Dnumber = number | `${number}`;