Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

63 changed files with 4630 additions and 7880 deletions

View file

@ -1,32 +0,0 @@
name: Build dev
on:
push:
jobs:
build:
runs-on: docker
steps:
# Setup
- uses: https://code.forgejo.org/actions/setup-node@v3
with:
node-version: 18
- uses: https://code.forgejo.org/actions/checkout@v3
- name: Install dependencies
run: npm install -D
# Compile
- name: Build project
if: success()
run: npx nuxi build
# Upload
- name: Package to tar
if: success()
run: tar --create --file build.tar .output/*
- name: Upload build
if: success()
uses: https://code.forgejo.org/actions/upload-artifact@v3
with:
name: WorkshopTasker-${{ github.ref_name }}-${{ github.run_number }}.tar
path: build.tar

View file

@ -6,26 +6,18 @@ Allows for managing order tasks and imported products for a small workshop.
# Running # Running
The project was tested with the nodejs version v18.17.1. It may not work with versions higher than 18 of nodejs. 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 settings. All provided variables below are required. Project uses Oracle MySQL as a database store. It uses environment variables for the connection settings. All provided variables below are required.
| Environment variable | Description | | Environment variable | Description |
|----------------------|-------------------| |----------------------|-------------------|
| `DB_URL` | Database url, see [this](https://www.prisma.io/docs/concepts/database-connectors/mysql#connection-url) | | `DB_HOST` | Database host |
| `DB_PORT` | Database port |
| `DB_USER` | Database user |
| `DB_PASSWORD` | Database password |
| `DB_SCHEMA` | Database schema |
## From Repository After setting variables, you can run the project using `npx nuxi dev` or `npx nuxi preview`.
Dev dependencies are required. `npm install -D`.
After setting variables, you can run the project using `npx nuxi dev`.
You can also setup variables by placing .env file in the root of the repository.
When using nix, you can use `nix-shell` to get prisma engines installed in your environment.
## From Actions
You can download build from [repository actions](https://git.proot.pl/Wroclaw/WorkshopTasker/actions?workflow=build.yml).
You need to extract tar file.
After setting environment variables, you can launch the project using the `node server/index.mjs` command.

View file

@ -1,29 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import FormClient from "~/components/formClient.vue"; type optionalMap<Optional> = Optional extends true ? undefined : string | number;
// type typeMap<Type extends string = {};
type optionalMap<Optional, type> = Optional extends true ? undefined | type : type; export type fieldDefinition<Optional extends boolean = boolean> = {
type typeMap = {
"text": string,
"password": string,
"number": number,
"boolean": boolean,
"none": undefined,
"client": `${bigint}`
};
export type fieldDefinition<Optional extends boolean = boolean, type extends keyof typeMap = keyof typeMap> = {
key: string, key: string,
label?: string, label?: string,
type: type, type: "text" | "number",
optional?: Optional, optional?: Optional,
value?: optionalMap<Optional, typeMap[type]>, value?: optionalMap<Optional>,
} }
const props = defineProps<{ const props = defineProps<{
fields: Array<fieldDefinition>, fields: Array<fieldDefinition>,
modelValue?: any,
}>(); }>();
// eslint-disable-next-line func-call-spacing // eslint-disable-next-line func-call-spacing
@ -32,7 +23,7 @@ const emit = defineEmits<{
(e: "updateSubModelValue", key: fieldDefinition["key"], value: fieldDefinition["value"]): void, (e: "updateSubModelValue", key: fieldDefinition["key"], value: fieldDefinition["value"]): void,
}>(); }>();
const modelValue = ref<{[key: string]: string | number | boolean | undefined}>({}); const modelValue = ref<{[key: string]: string | number | undefined}>({});
for (const i of props.fields) { for (const i of props.fields) {
modelValue.value[i.key] = i.value; modelValue.value[i.key] = i.value;
@ -49,43 +40,12 @@ emit("update:modelValue", modelValue.value);
</script> </script>
<template> <template>
<div v-for="i of fields" :key="i.key">
<VTextField <VTextField
v-if="i.type == 'text'" v-for="i of fields"
:key="i.key"
:model-value="modelValue[i.key]" :model-value="modelValue[i.key]"
:label="i.label" :label="i.label"
type="text" :type="i.type"
@update:model-value="v => updateModel(i.key, v)" @update:model-value="v => updateModel(i.key, v)"
/> />
<VTextField
v-if="i.type == 'password'"
:model-value="modelValue[i.key]"
:label="i.label"
type="password"
@update:model-value="v => updateModel(i.key, v)"
/>
<VTextField
v-if="i.type == 'number'"
:model-value="modelValue[i.key]"
:label="i.label"
type="number"
@update:model-value="v => updateModel(i.key, Number(v))"
/>
<v-checkbox
v-if="i.type == 'boolean'"
:label="i.label"
:model-value="modelValue[i.key]"
@update:model-value="v => updateModel(i.key, Boolean(v))"
/>
<p v-if="i.type == 'none'">
{{ i.label }}
</p>
<FormClient
v-if="i.type == 'client'"
:model-value="modelValue[i.key] as `${bigint}`"
:label="i.label"
:optional="i.optional"
@update:model-value="v => updateModel(i.key, v)"
/>
</div>
</template> </template>

View file

@ -1,37 +0,0 @@
<script setup lang="ts">
import { useFetch, createError } from '#app';
const props = defineProps<{
label?: string,
optional?: boolean,
modelValue?: `${bigint}`,
}>();
// eslint-disable-next-line func-call-spacing
const emit = defineEmits<{
(e: "update:modelValue", value: `${bigint}`): void,
}>();
// FIXME: allow to search all clients instead of newest 50 (needs api call)
const clientsRequest = await useFetch("/api/clients");
if (clientsRequest.error.value) throw createError(clientsRequest.error.value?.data ?? "");
const clients = clientsRequest.data.value?.map((e) => {
return {
value: e.id,
title: e.name ?? `[null] (${e.id})`,
props: {
subtitle: e.address,
},
};
}) ?? [];
</script>
<template>
<v-autocomplete
:label="label ?? 'Client'"
:model-value="modelValue"
:items="clients"
:clearable="optional"
@update:model-value="v => emit('update:modelValue', v)"
/>
</template>

View file

@ -1,22 +1,10 @@
<script setup> <script setup>
import { useDisplay, useTheme } from "vuetify/lib/framework.mjs"; import { useDisplay } from "vuetify/lib/framework.mjs";
import { ref, watch } from "vue"; import { ref } from "vue";
import { navigateTo, useRouter } from "#app"; import { navigateTo } from "#app";
const route = useRouter().currentRoute;
const { mobile } = useDisplay(); const { mobile } = useDisplay();
const navOpen = ref(!mobile.value); const navOpen = ref(!mobile.value);
const navSelected = ref([route.value.path]);
watch(route, (v) => {
navSelected.value = [v.path];
});
const theme = useTheme();
function switchTheme() {
theme.global.name.value = theme.global.current.value.dark ? 'light' : 'dark';
}
</script> </script>
<template> <template>
@ -25,17 +13,22 @@ function switchTheme() {
<VToolbarTitle>Database Project</VToolbarTitle> <VToolbarTitle>Database Project</VToolbarTitle>
</VAppBar> </VAppBar>
<VNavigationDrawer v-model="navOpen" :temporary="mobile"> <VNavigationDrawer v-model="navOpen" :temporary="mobile">
<VList v-model:selected="navSelected" density="compact" nav> <!-- <VList>
<VListItem
prepend-avatar="https://cdn.discordapp.com/avatars/317001025074757632/248f503c44bc95ac75f55f5835e6ff9f.png?size=512"
title="Anonymous"
/>
</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')" /> <VListItem prepend-icon="mdi-account" title="Clients" value="/clients" @click="navigateTo('/clients')" />
<VListItem prepend-icon="mdi-receipt-text" title="Orders" value="/orders" @click="navigateTo('/orders')" />
<VDivider /> <VDivider />
<VListItem prepend-icon="mdi-home" title="Home" value="/" @click="navigateTo('/')" />
<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> </VList>
<template #append>
<VList v-model:selected="navSelected" density="compact" nav>
<VDivider />
<VListItem prepend-icon="mdi-theme-light-dark" title="Switch theme" class="mx-auto" @click="switchTheme()" />
<VListItem prepend-icon="mdi-login" title="My account" value="/login" @click="navigateTo('/login')" />
</VList>
</template>
</VNavigationDrawer> </VNavigationDrawer>
</template> </template>

View file

@ -1,10 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { useFetch } from '#imports'; import { order as orderType } from '~/utils/types/database';
type Order = Awaited<ReturnType<typeof useFetch<void, any, "/api/orders/:id", "get">>>["data"]["value"];
const props = defineProps<{ const props = defineProps<{
order?: Order | undefined order?: orderType
}>(); }>();
</script> </script>
@ -26,7 +24,7 @@ const props = defineProps<{
</thead> </thead>
<tbody> <tbody>
<tr v-for="i in props.order.work" :key="i.id"> <tr v-for="i in props.order.work" :key="i.id">
<td>{{ i.fulfilled }}</td> <td>{{ i.is_fulfilled }}</td>
<td> <td>
{{ i.offer.name }} {{ i.offer.name }}
</td> </td>

11
components/test.vue Normal file
View file

@ -0,0 +1,11 @@
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
<template>
<VBtn @click="count++">
Clicky {{ count }} times
</VBtn>
</template>

View file

@ -1,8 +0,0 @@
import { defineNuxtRouteMiddleware, navigateTo, useFetch } from "nuxt/app";
export default defineNuxtRouteMiddleware(async (to, from) => {
const firstRun = await useFetch("/api/firstRun");
if (firstRun.data.value)
return navigateTo({ path: "/firstRun" });
});

View file

@ -1,19 +1,10 @@
import { defineNuxtConfig } from "nuxt/config"; import { defineNuxtConfig } from "nuxt/config";
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify';
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
pages: true, pages: true,
build: { build: {
transpile: ["vuetify"], transpile: ["vuetify"],
}, },
modules: [
(_options, nuxt) => {
nuxt.hooks.hook('vite:extendConfig', (config) => {
config.plugins?.push(vuetify({ autoImport: true }));
});
},
],
css: [ css: [
"vuetify/lib/styles/main.sass", "vuetify/lib/styles/main.sass",
"@mdi/font/css/materialdesignicons.min.css", "@mdi/font/css/materialdesignicons.min.css",
@ -27,11 +18,7 @@ export default defineNuxtConfig({
}, },
}, },
vite: { vite: {
vue: { // devBundler: "legacy"
template: {
transformAssetUrls,
},
},
}, },
imports: { imports: {
autoImport: false, autoImport: false,

9769
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,18 +13,15 @@
"devDependencies": { "devDependencies": {
"@mdi/font": "^7.2.96", "@mdi/font": "^7.2.96",
"@nuxtjs/eslint-config-typescript": "^12.0.0", "@nuxtjs/eslint-config-typescript": "^12.0.0",
"@typescript-eslint/eslint-plugin": "^6.9.1", "@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^6.9.1", "@typescript-eslint/parser": "^5.59.0",
"eslint": "^8.39.0", "eslint": "^8.39.0",
"nuxt": "3.8.2", "nuxt": "3.4.0",
"prisma": "5.7.1",
"sass": "^1.62.0", "sass": "^1.62.0",
"vite-plugin-vuetify": "^2.0.1", "vite-plugin-vuetify": "^1.0.2",
"vuetify": "^3.1.15" "vuetify": "^3.1.15"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "5.7.1", "mysql2": "^3.2.3"
"@prisma/engines": "5.7.1",
"@prisma/migrate": "5.7.1"
} }
} }

View file

@ -1,34 +1,29 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, type Ref } from "vue"; /* global $fetch */
import { useRoute, useFetch, createError } from "nuxt/app";
import { ref, Ref } from "vue";
import { VBtn, VForm } from "vuetify/components"; import { VBtn, VForm } from "vuetify/components";
import PagedList from "~/components/pagedList.vue"; import PagedList from "~/components/pagedList.vue";
import Snowflake from "~/utils/snowflake"; import Snowflake from "~/utils/snowflake";
import { client as clientType, order, orderSummary } from "~/utils/types/database";
import OrderView from "~/components/orderView.vue"; import OrderView from "~/components/orderView.vue";
import EntryEditor, { type fieldDefinition } from "~/components/entryEditor.vue"; import EntryEditor, { fieldDefinition } from "~/components/entryEditor.vue";
import { useRoute, useFetch, createError, useRequestFetch } from "#imports";
const route = useRoute(); const route = useRoute();
const fetch = useRequestFetch();
const id = route.params.id; const id = route.params.id;
const clientRequest = await useFetch(`/api/clients/${id}` as "/api/clients/:id"); const clientRequest = await useFetch(`/api/clients/${id}`);
if (clientRequest.error.value) throw createError(clientRequest.error.value?.data ?? ""); if (clientRequest.error.value) throw createError(clientRequest.error.value?.data ?? "");
type Client = NonNullable<typeof clientRequest.data.value>; const client = clientRequest.data as Ref<clientType>;
const client = clientRequest.data as Ref<Client>;
const clientOrdersRequest = await useFetch(`/api/clients/${id}/orders` as "/api/clients/:id/orders"); const clientOrdersRequest = await useFetch(`/api/clients/${id}/orders`);
if (clientOrdersRequest.error.value) throw createError(clientOrdersRequest.error.value?.data ?? ""); if (clientOrdersRequest.error.value) throw createError(clientOrdersRequest.error.value?.data ?? "");
type OrderSummary = NonNullable<typeof clientOrdersRequest.data.value>; const clientOrders = clientOrdersRequest.data as Ref<Array<orderSummary>>;
const clientOrders = clientOrdersRequest.data as Ref<OrderSummary>;
type Order = Awaited<ReturnType<typeof useFetch<void, any, "/api/orders/:id", "get">>>["data"]["value"];
// cache
const orders = ref<Map<string, { const orders = ref<Map<string, {
loading: boolean, loading: boolean,
value?: Order value?: order
}>>(new Map()); }>>(new Map());
for (const i of clientOrders.value) for (const i of clientOrders.value)
@ -39,9 +34,9 @@ async function loadOrder(id: string) {
if (!entry) throw createError(`excepted order entry for ${id}`); if (!entry) throw createError(`excepted order entry for ${id}`);
entry.loading = true; entry.loading = true;
// @ts-expect-error // @ts-expect-error
entry.value = await fetch(`/api/orders/${id}` as "/api/order/:id", { entry.value = await $fetch(`/api/orders/${id}` as "/api/order/:id", {
method: "GET", method: "GET",
}) as Order; });
entry.loading = false; entry.loading = false;
} }
@ -56,10 +51,10 @@ function updatePagedListVModel(element: Array<string>) {
const editMode = ref<boolean>(route.query?.edit === "1"); const editMode = ref<boolean>(route.query?.edit === "1");
function editorFields(): Array<fieldDefinition> { function editorFields(): Array<fieldDefinition> {
return [ return [
{ key: "name", type: "text", label: "Name", value: client.value.name ?? "" }, { key: "name", type: "text", label: "Name", value: client.value.name ?? undefined },
{ key: "address", type: "text", label: "Address", value: client.value.address ?? "" }, { key: "address", type: "text", label: "Address", value: client.value.address ?? undefined },
{ key: "phone", type: "text", label: "Phone", value: client.value.phone ?? "" }, { key: "phone", type: "text", label: "Phone", value: client.value.phone ?? undefined },
{ key: "email", type: "text", label: "E-mail", value: client.value.email ?? "" }, { key: "email", type: "text", label: "E-mail", value: client.value.email ?? undefined },
]; ];
} }
@ -78,7 +73,7 @@ async function handleSubmit() {
submitting.value = true; submitting.value = true;
normalizeForm(); normalizeForm();
try { try {
const result = await fetch( const result = await $fetch(
`/api/clients/${client.value.id}` as "/api/clients/:id", { `/api/clients/${client.value.id}` as "/api/clients/:id", {
method: "PATCH", method: "PATCH",
body: formData.value, body: formData.value,
@ -189,24 +184,24 @@ function getCreationDate() {
> >
<template #title="i"> <template #title="i">
<VRow> <VRow>
<VCol>{{ new Date(Number(new Snowflake(BigInt((i.record.id))).timestamp)).toLocaleDateString() }}</VCol> <VCol>{{ new Date(Number(new Snowflake(BigInt(((i.record) as orderSummary).id)).timestamp)).toLocaleDateString() }}</VCol>
<VCol>{{ i.record.value }} PLN</VCol> <VCol>{{ ((i.record) as orderSummary).value }} PLN</VCol>
<VCol> <VCol>
{{ i.record.imported_products_count }} {{ ((i.record) as orderSummary).imported_products_count }}
products, products,
{{ i.record.work_count }} {{ ((i.record) as orderSummary).work_count }}
works works
</VCol> </VCol>
</VRow> </VRow>
</template> </template>
<template #text="i"> <template #text="i">
<VProgressLinear <VProgressLinear
:height="orders.get(i.record.id)?.loading ?? true ? undefined : 0" :height="orders.get((i.record as orderSummary).id)?.loading ?? true ? undefined : 0"
absolute absolute
:progress="orders.get(i.record.id)?.loading ?? true" :progress="orders.get((i.record as orderSummary).id)?.loading ?? true"
:indeterminate="orders.get(i.record.id)?.loading ?? true" :indeterminate="orders.get((i.record as orderSummary).id)?.loading ?? true"
/> />
<OrderView :order="orders.get(i.record.id)?.value" /> <OrderView :order="(orders.get((i.record as orderSummary).id)?.value as order | undefined)" />
</template> </template>
</PagedList> </PagedList>
</VCol> </VCol>

124
pages/clients.backup.vue Normal file
View file

@ -0,0 +1,124 @@
<script setup lang="ts">
/* global $fetch */
import { useFetch, createError } from "nuxt/app";
import { ref, Ref } from "vue";
import { definePageMeta } from "~/.nuxt/imports";
import { client as clientType } from "~/utils/types/database";
definePageMeta({ middleware: ["auth"] });
const clientsRequest = await useFetch("/api/clients");
if (clientsRequest.error.value) throw createError(clientsRequest.error.value?.data ?? "");
const clients = clientsRequest.data as Ref<NonNullable<typeof clientsRequest.data.value>>;
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>>;
function rowClicked(client: string, edit = false) {
console.log(client);
}
async function rowDelete(client: string) {
try {
await $fetch<clientType>(`/api/clients/${client}`, {
method: "DELETE",
});
clients.value = clients.value.filter(e => e.id !== client);
count.value.count--;
} catch (e) {
// FIXME: show the error
console.log(e);
}
}
const loadingMore = ref<boolean>(false);
async function loadBefore() {
loadingMore.value = true;
clients.value.push(...await $fetch("/api/clients", {
query: {
before: clients.value[clients.value.length - 1].id,
},
}));
loadingMore.value = false;
}
</script>
<template>
<VOverlay
model-value
origin="top center"
:scrim="false"
height="fit-content"
persistent
no-click-animation
>
<VAlert class="alert">
owowowowowowowowo
</VAlert>
</VOverlay>
<VRow>
<VCol>
<VBreadcrumbs :items="['Clients']" />
<VSpacer />
<div class="text-h4">
There are {{ count?.count }} clients in the database.
</div>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VTable>
<thead>
<tr>
<th>Name</th>
<th>Address</th>
<th />
</tr>
</thead>
<tbody>
<tr v-for="client in clients" :key="client.id">
<td @click="() => rowClicked(client.id)">
{{ client.name }}
</td>
<td @click="() => rowClicked(client.id)">
{{ client.address }}
</td>
<td class="buttons">
<div>
<VBtn icon="mdi-pencil" variant="text" @click="() => rowClicked(client.id, true)" />
<VBtn icon="mdi-delete" color="red" variant="text" @click="() => rowDelete(client.id)" />
</div>
</td>
</tr>
</tbody>
</VTable>
</VCard>
<VCol>
<VBtn
v-if="clients.length < count.count"
color="primary"
:loading="loadingMore"
@click="loadBefore"
>
Load more
</VBtn>
</VCol>
</VCol>
</VRow>
</template>
<style scoped>
.buttons {
width: 0;
padding-right: 4px !important;
}
.buttons > div {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
</style>

View file

@ -1,17 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { type NuxtError } from "nuxt/app"; /* global $fetch */
import { ref, type Ref, reactive } from "vue"; import { useFetch, createError, navigateTo, NuxtError, useRoute } from "nuxt/app";
import { ref, Ref, reactive } from "vue";
import { VBtn } from "vuetify/components"; import { VBtn } from "vuetify/components";
import { definePageMeta } from "~/.nuxt/imports";
import { client as clientType } from "~/utils/types/database";
import pagedTable from "~/components/pagedTable.vue"; import pagedTable from "~/components/pagedTable.vue";
import Alerts, { type AlertData } from "~/components/alerts.vue"; import Alerts, { AlertData } from "~/components/alerts.vue";
import { type fieldDefinition } from "~/components/entryEditor.vue"; import { fieldDefinition } from "~/components/entryEditor.vue";
import { useFetch, createError, navigateTo, useRoute, definePageMeta, useRequestFetch } from "#imports";
definePageMeta({ middleware: ["auth"] }); definePageMeta({ middleware: ["auth"] });
const route = useRoute(); const route = useRoute();
const fetch = useRequestFetch();
const alerts = ref<Array<AlertData>>([]); const alerts = ref<Array<AlertData>>([]);
@ -32,7 +32,7 @@ async function rowClicked(client: string, edit = false) {
async function rowDelete(client: string) { async function rowDelete(client: string) {
try { try {
await fetch(`/api/clients/${client}` as "/api/clients/:id", { await $fetch<clientType>(`/api/clients/${client}`, {
method: "DELETE", method: "DELETE",
}); });
clients.value = clients.value.filter(e => e.id !== client); clients.value = clients.value.filter(e => e.id !== client);
@ -48,7 +48,7 @@ async function loadBefore() {
loadingMore.value = true; loadingMore.value = true;
try { try {
clients.value.push(...await fetch("/api/clients", { clients.value.push(...await $fetch("/api/clients", {
query: { query: {
before: clients.value[clients.value.length - 1].id, before: clients.value[clients.value.length - 1].id,
}, },
@ -84,21 +84,19 @@ const formData = ref<any>({
function normalizeForm() { function normalizeForm() {
for (const i in formData.value) for (const i in formData.value)
formData.value[i] = formData.value[i] === "" || formData.value[i] === undefined ? null : formData.value[i]; formData.value[i] = formData.value[i] === "" ? null : formData.value[i];
} }
async function handleSubmit() { async function handleSubmit() {
submitting.value = true; submitting.value = true;
normalizeForm(); normalizeForm();
try { try {
const result = await fetch( const result = await $fetch(
"/api/clients", { "/api/clients/", {
method: "POST", method: "POST",
body: formData.value, body: formData.value,
}, },
); );
clients.value.unshift(result);
count.value.count++;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
submitting.value = false; submitting.value = false;

View file

@ -1,61 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue';
import { type NuxtError } from 'nuxt/app';
import { navigateTo, useFetch, definePageMeta, useRequestFetch } from '#imports';
import EntryEditor, { type fieldDefinition } from '~/components/entryEditor.vue';
import Alerts, { type AlertData } from '~/components/alerts.vue';
const editorFields: Array<fieldDefinition> = [
{ key: "username", type: "text", label: "Username", optional: false },
{ key: "password", type: "password", label: "Password", optional: false },
{ key: "email", type: "text", label: "email", optional: false },
];
const formValue = ref<any>({});
const alerts = ref<Array<AlertData>>([]);
definePageMeta({
layout: false,
});
async function submit() {
try {
await useRequestFetch()("/api/firstRun", {
body: formValue.value,
method: "POST",
});
await navigateTo("/login");
} catch (e) {
console.log(e);
alerts.value.push({ text: (e as NuxtError).data.message });
}
}
if (!(await useFetch("/api/firstRun")).data.value)
await navigateTo("/login");
</script>
<template>
<Alerts :alerts="alerts" />
<VCard max-width="450px" class="mx-auto mt-16" variant="outlined">
<template #title>
Initial setup
</template>
<template #text>
<p>
It looks like you've run the server with an empty or uninitialized database or with database without any users.<br>
Below you can initialize the database register your first user and.
</p><br>
<EntryEditor
:fields="editorFields"
@update-sub-model-value="(k, v) => { formValue[k] = v }"
/>
</template>
<template #actions>
<VBtn color="primary" @click="submit">
Initialize
</VBtn>
</template>
</VCard>
</template>

28
pages/forms.vue Normal file
View file

@ -0,0 +1,28 @@
<script setup lang="ts">
/* global $fetch */
import { ref } from 'vue';
const question = ref();
const answer = ref();
async function getAnswer() {
const message = await $fetch("/api/echo", {
body: question.value,
method: "POST",
});
answer.value = message;
}
</script>
<template>
<VCard width="400px">
<template #text>
<VTextarea v-model="question" /><br>
<p>{{ answer }}</p>
<VBtn @click="getAnswer">
Send
</VBtn>
</template>
</VCard>
</template>

View file

@ -1,9 +1,23 @@
<script setup lang="ts"> <script setup>
import { navigateTo } from '#imports'; import Test from '~/components/test.vue';
navigateTo("/clients");
</script> </script>
<template> <template>
<VBtn @click="navigateTo('/clients')" /> <h1>Hi mom!</h1>
<br>
<Test />
<Test />
<Test />
<Test />
<Test /><br>
<VCard
text="..."
variant="outlined"
/>
</template> </template>
<style scoped>
h1 {
color: blue;
}
</style>

View file

@ -1,39 +1,35 @@
<script setup lang="ts"> <script setup lang="ts">
/* global $fetch */
import { ref, watch } from "vue"; import { ref, watch } from "vue";
import { VForm } from "vuetify/components"; import { VForm } from "vuetify/components";
import { type CookieRef } from "#app"; import { navigateTo, useCookie, useFetch, useRoute } from "nuxt/app";
import { cookieSettings } from "~/utils/cookieSettings"; import { cookieSettings } from "~/utils/cookieSettings";
import { definePageMeta, navigateTo, useCookie, useFetch, useRoute, useRequestFetch, useRequestEvent } from "#imports"; import { definePageMeta } from "~/.nuxt/imports";
const route = useRoute(); const route = useRoute();
const fetch = useRequestFetch();
const login = ref(""); const login = ref("");
const password = ref(""); const password = ref("");
const loading = ref(false); const loading = ref(false);
const error = ref<true | string>(true); const error = ref<true | string>(true);
const form = ref<VForm | null>(null); const form = ref<VForm | null>(null);
const tokenCookie = useCookie("token", cookieSettings) as CookieRef<string | undefined>; const loggedIn = ref<boolean>(useCookie("token", cookieSettings).value != null);
const loggedIn = ref<boolean>(tokenCookie.value !== undefined);
watch(tokenCookie, (v) => { loggedIn.value = v !== undefined; });
const redirectTo = ref(route.redirectedFrom); const redirectTo = ref(route.redirectedFrom);
definePageMeta({ definePageMeta({
layout: false, layout: false,
middleware: ["first-run"],
}); });
async function submit() { async function submit() {
loading.value = true; loading.value = true;
try { try {
const result = await fetch("/api/login", { const result = await $fetch("/api/login", {
body: { login: login.value, password: password.value }, body: { login: login.value, password: password.value },
method: "POST", method: "POST",
}); });
console.log(result); console.log(result);
tokenCookie.value = result.token; loggedIn.value = true;
password.value = ""; password.value = "";
} catch (e) { } catch (e) {
console.log(typeof e); console.log(typeof e);
@ -48,10 +44,9 @@ async function submit() {
async function logout() { async function logout() {
try { try {
await fetch("/api/logout"); await $fetch("/api/logout");
loggedIn.value = false; loggedIn.value = false;
} catch (e) { } catch (e) {
tokenCookie.value = undefined;
console.error(e); console.error(e);
} }
} }
@ -61,7 +56,7 @@ watch(loggedIn, updateUserInfo);
async function updateUserInfo() { async function updateUserInfo() {
if (loggedIn.value) { if (loggedIn.value) {
try { try {
userInfo.value = JSON.stringify(await fetch("/api/users/me")); userInfo.value = JSON.stringify(await $fetch("/api/users/me"));
} catch (e) { } catch (e) {
// expected if the user is not logged in // expected if the user is not logged in
userInfo.value = ""; userInfo.value = "";
@ -71,7 +66,7 @@ async function updateUserInfo() {
} }
} }
await updateUserInfo(); updateUserInfo();
</script> </script>
<template> <template>

View file

@ -1,194 +0,0 @@
<script setup lang="ts">
import { ref, type Ref } from "vue";
import { VBtn } from "vuetify/components";
import type { NuxtError } from "#app";
import Alerts, { type AlertData } from "~/components/alerts.vue";
import { type fieldDefinition } from "~/components/entryEditor.vue";
import { definePageMeta, useFetch, createError, useRoute, navigateTo, useRequestFetch } from "#imports";
definePageMeta({ middleware: ["auth"] });
const route = useRoute();
const fetch = useRequestFetch();
const alerts = ref<Array<AlertData>>([]);
const ordersRequest = await useFetch("/api/orders");
if (ordersRequest.error.value) throw createError(ordersRequest.error.value?.data ?? "");
const orders = ordersRequest.data as Ref<NonNullable<typeof ordersRequest.data.value>>;
const countRequest = await useFetch("/api/orders/count");
if (countRequest.error.value) throw createError(countRequest.error.value?.data ?? "");
const count = countRequest.data as Ref<NonNullable<typeof countRequest.data.value>>;
const createMode = ref<boolean>(route.query?.create === "1");
async function rowClicked(client: string, edit = false) {
await navigateTo({
path: `/order/${client}`,
query: { edit: edit ? 1 : undefined },
});
}
async function rowDelete(client: string) {
try {
await fetch(`/api/orders/${client}` as "/api/orders/:id", {
method: "DELETE",
});
orders.value = orders.value.filter(e => e.id !== client);
count.value.count--;
} catch (e) {
alerts.value.push({ text: (e as NuxtError).message, type: "error" });
console.log(e);
}
}
const loadingMore = ref<boolean>(false);
async function loadBefore() {
loadingMore.value = true;
try {
orders.value.push(...await fetch("/api/orders", {
query: {
before: orders.value[orders.value.length - 1].id,
},
}));
} catch (e) {
alerts.value.push({ text: (e as NuxtError).message });
console.error(e);
}
loadingMore.value = false;
}
function editorFields(): fieldDefinition[] {
return [
{ key: "clientId", type: "client", label: "Client" },
{ key: "draft", type: "boolean", label: "Draft", value: true },
{ key: "imported_products", type: "none", label: "imported_produts - TBA" },
{ key: "work", type: "none", label: "work - TBA" },
];
}
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];
formData.value.imported_products = [];
formData.value.work = [];
}
async function handleSubmit() {
submitting.value = true;
normalizeForm();
try {
const result = await fetch(
"/api/orders", {
method: "POST",
body: formData.value,
},
);
orders.value.unshift({
...result,
imported_products_count: result.imported_products.length,
work_count: result.work.length,
// NOTE: currently all newly created orders on this page are valued zero
value: 0,
});
count.value.count++;
} 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"
>
<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>
<VRow>
<VCol>
<VBreadcrumbs :items="['Orders']" />
<VSpacer />
<div class="text-h4">
There are {{ count?.count }} orders in the database.
</div>
<VBtn
ref="formButton"
>
Create
</VBtn>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VTable>
<thead>
<tr>
<th>Client</th>
<th>Draft</th>
<th />
</tr>
</thead>
<pagedTable
buttons
:records="orders"
:fields="['client', 'Draft']"
record-key="id"
@click="(id) => rowClicked(id, false)"
@click:edit="(id) => rowClicked(id, true)"
@click:delete="(id) => rowDelete(id)"
/>
</VTable>
</VCard>
<VCol>
<VBtn
v-if="orders.length < count.count"
color="primary"
:loading="loadingMore"
@click="loadBefore"
>
Load more
</VBtn>
</VCol>
</VCol>
</VRow>
</template>

50
pages/tableExample.vue Normal file
View file

@ -0,0 +1,50 @@
<script setup lang="ts">
import { useFetch } from '#app';
import { ref } from 'vue';
const tableContent = ref(await useFetch("/api/dbtest").data);
</script>
<template>
<v-card width="min-content">
<v-table>
<thead>
<tr>
<th>id</th>
<th>nazwa</th>
<th>adres</th>
<th>kontakt</th>
<th />
</tr>
</thead>
<tbody>
<tr v-for="row in tableContent" :key="row.id">
<td>{{ row.id }}</td>
<td>{{ row.nazwa }}</td>
<td>{{ row.adres }}</td>
<td>{{ row.kontakt }}</td>
<td>
<v-btn-group variant="plain">
<v-btn icon="mdi-pencil" color="main" />
<v-btn icon="mdi-delete" color="red" />
</v-btn-group>
</td>
</tr>
<tr class="last">
<td>*</td>
<td><v-text-field label="nazwa" density="compact" /></td>
<td><v-text-field label="adres" density="compact" /></td>
<td><v-text-field label="kontakt" density="compact" /></td>
<td><v-btn icon="mdi-plus" variant="plain" class="rounded" /></td>
</tr>
</tbody>
</v-table>
</v-card>
</template>
<style scoped>
.last td:not(:last-child):not(:first-child) {
padding-left: 0;
padding-right: 0;
}
</style>

View file

@ -6,6 +6,8 @@ import { defineNuxtPlugin } from '#app';
export default defineNuxtPlugin((nuxtApp) => { export default defineNuxtPlugin((nuxtApp) => {
const vuetify = createVuetify({ const vuetify = createVuetify({
ssr: true, ssr: true,
components,
directives,
theme: { theme: {
}, },
}); });

View file

@ -1,118 +0,0 @@
datasource db {
provider = "mysql"
url = env("DB_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id BigInt @id @unique @default(dbgenerated("(((unix_timestamp() * 1000) * pow(2,22)) + floor((rand() * pow(2,12))))")) @db.UnsignedBigInt
username String @unique @db.VarChar(30)
email String @unique @db.VarChar(128)
password Bytes @db.Binary(64)
display_name String? @db.VarChar(30)
managedOrders Order[]
sessions Session[]
@@map("users")
}
model Session {
id BigInt @id @default(dbgenerated("(((unix_timestamp() * 1000) * pow(2,22)) + floor((rand() * pow(2,12))))")) @db.UnsignedBigInt
userId BigInt @map("user") @db.UnsignedBigInt
sessionToken Bytes @db.Binary(64)
expiry_date DateTime? @default(dbgenerated("(now() + interval 30 day)")) @db.Timestamp(0)
user User @relation(fields: [userId], references: [id])
@@index([userId], map: "user_idx")
@@index([sessionToken])
@@map("sessions")
}
model Client {
id BigInt @id @default(dbgenerated("(((unix_timestamp() * 1000) * pow(2,22)) + floor((rand() * pow(2,12))))")) @db.UnsignedBigInt
name String? @db.VarChar(128)
address String? @db.VarChar(128)
phone String? @db.VarChar(16)
email String? @db.VarChar(128)
orders Order[]
@@map("clients")
}
model Order {
id BigInt @id @default(dbgenerated("(((unix_timestamp() * 1000) * pow(2,22)) + floor((rand() * pow(2,12))))")) @db.UnsignedBigInt
clientId BigInt @db.UnsignedBigInt @map("client")
userId BigInt @db.UnsignedBigInt @map("user")
draft Boolean @default(true) @map("is_draft") @db.TinyInt
imported_products ImportedProduct[]
client Client @relation(fields: [clientId], references: [id])
user User @relation(fields: [userId], references: [id])
work Work[]
@@index([clientId])
@@index([userId])
@@map("orders")
}
model ImportedProduct {
id BigInt @id @default(dbgenerated("(((unix_timestamp() * 1000) * pow(2,22)) + floor((rand() * pow(2,12))))")) @db.UnsignedBigInt
orderId BigInt @db.UnsignedBigInt @map("order")
name String? @db.VarChar(128)
link String @db.VarChar(1024)
price_imported Decimal @default(0.00) @db.Decimal(10, 2)
price Decimal @default(0.00) @db.Decimal(10, 2)
order Order @relation(fields: [orderId], references: [id])
@@index([orderId])
@@map("imported_products")
}
model Offer {
id BigInt @id @default(dbgenerated("(((unix_timestamp() * 1000) * pow(2,22)) + floor((rand() * pow(2,12))))")) @db.UnsignedBigInt
name String @db.VarChar(45)
description String? @db.Text
recommended_price Decimal? @db.Decimal(10, 2)
work Work[]
@@map("offer")
}
model OrderTemplate {
id BigInt @id @default(dbgenerated("(((unix_timestamp() * 1000) * pow(2,22)) + floor((rand() * pow(2,12))))")) @db.UnsignedBigInt
name String @db.VarChar(45)
description String? @db.Text
work_templates WorkTemplate[]
@@map("order_templates")
}
model Work {
id BigInt @id @default(dbgenerated("(((unix_timestamp() * 1000) * pow(2,22)) + floor((rand() * pow(2,12))))")) @db.UnsignedBigInt
orderId BigInt @map("order") @db.UnsignedBigInt
offerId BigInt @map("offer") @db.UnsignedBigInt
price Decimal @db.Decimal(10, 2)
notes String? @db.Text
fulfilled Boolean @default(false) @map("is_fulfilled") @db.TinyInt
order Order @relation(fields: [orderId], references: [id])
offer Offer @relation(fields: [offerId], references: [id])
@@index([offerId], map: "offer_idx")
@@index([orderId], map: "order_idx")
@@map("work")
}
model WorkTemplate {
id BigInt @id @default(dbgenerated("(((unix_timestamp() * 1000) * pow(2,22)) + floor((rand() * pow(2,12))))")) @db.UnsignedBigInt
orderTemplateId BigInt @map("order_template") @db.UnsignedBigInt
offerId BigInt @map("offer") @db.UnsignedBigInt
price Decimal @default(0.00) @db.Decimal(10, 2)
notes String? @db.Text
orderTemplate OrderTemplate @relation(fields: [orderTemplateId], references: [id])
@@index([offerId])
@@index([orderTemplateId])
@@map("work_templates")
}

Binary file not shown.

View file

@ -1,10 +1,10 @@
import { defineEventHandler } from "h3"; /* global defineEventHandler getQuery, createError */
import getPaginatedParameters from "../utils/baaPageParsing"; import BaaPagination from "~/server/utils/baaPagination";
import { database } from "../utils/database"; import { client } from "~/utils/types/database";
import { prismaToWeb } from "~/server/utils/prismaToWeb";
export const baaWrapper = new BaaPagination<client, "id">("clients", "id");
export default defineEventHandler((e) => { export default defineEventHandler((e) => {
const pageParameters = getPaginatedParameters(e, 50, 200); return baaWrapper.RESTget(e);
return database.client.findPaginated(pageParameters, {}).then(prismaToWeb);
}); });

View file

@ -1,12 +1,7 @@
import { defineEventHandler, readBody, setResponseStatus } from "h3"; /* global defineEventHandler, createError, readBody */
import { type Client } from "@prisma/client";
import getRequestingUser from "../utils/getRequestingUser"; import { baaWrapper } from "./clients.get";
import { database } from "../utils/database"; import { client } from "~/utils/types/database";
import { prismaToWeb } from "~/server/utils/prismaToWeb";
import Snowflake from "~/utils/snowflake";
import { createError } from "#imports";
const clientKeys: Array<string> = [ const clientKeys: Array<string> = [
"name", "name",
@ -15,10 +10,10 @@ const clientKeys: Array<string> = [
"email", "email",
]; ];
export function checkIsClient<Patch extends boolean = boolean>( export function checkIsClient(
value: any, value: any,
patch: Patch, required = false,
): value is Patch extends true ? Partial<Omit<Client, "id">> : Omit<Client, "id"> { ): value is Partial<Omit<client, "id">> {
const errors = new Map<string, string>(); const errors = new Map<string, string>();
if (typeof value !== "object") { if (typeof value !== "object") {
@ -28,12 +23,12 @@ export function checkIsClient<Patch extends boolean = boolean>(
}); });
} }
if (!(typeof value.name === "string" || value.name === null || (patch && value.name === undefined))) errors.set("name", "is not string or null"); if (!(typeof value.name === "string" || value.name === null || (!required && value.name === undefined))) errors.set("name", "is not string or null");
if (!(typeof value.address === "string" || value.address === null || (patch && value.address === undefined))) errors.set("address", "is not string or null"); if (!(typeof value.address === "string" || value.address === null || (!required && value.address === undefined))) errors.set("address", "is not string or null");
if (!(typeof value.phone === "string" || value.phone === null || (patch && value.phone === undefined))) errors.set("phone", "is not string or null"); if (!(typeof value.phone === "string" || value.phone === null || (!required && value.phone === undefined))) errors.set("phone", "is not string or null");
if (!(typeof value.email === "string" || value.email === null || (patch && value.email === undefined))) errors.set("email", "is not string or null"); if (!(typeof value.email === "string" || value.email === null || (!required && value.email === undefined))) errors.set("email", "is not string or null");
for (const i in value as Partial<Omit<Client, "id">>) for (const i in value as Partial<Omit<client, "id">>)
if (!clientKeys.includes(i)) errors.set(i, `excessive property`); if (!clientKeys.includes(i)) errors.set(i, `excessive property`);
if (errors.size !== 0) { if (errors.size !== 0) {
@ -53,20 +48,6 @@ export function checkIsClient<Patch extends boolean = boolean>(
return true; return true;
} }
export default defineEventHandler(async (e) => { export default defineEventHandler((e) => {
const body = await readBody(e); return baaWrapper.RESTpost(e, clientKeys as Array<keyof Omit<client, "id">>, (o): o is Omit<client, "id"> => checkIsClient(o, true));
const id = new Snowflake().state;
const user = await getRequestingUser(e);
if (!checkIsClient(body, false)) throw createError({ message: "Invalid body", statusCode: 400 });
const rvalue = await database.client.create({
data: {
...body,
id,
},
});
setResponseStatus(e, 201);
return prismaToWeb(rvalue);
}); });

View file

@ -1,22 +1,7 @@
import { defineEventHandler } from "h3"; /* global defineEventHandler */
import { database } from "~/server/utils/database"; import { baaWrapper } from "../clients.get";
import { createError } from "#imports"; export default defineEventHandler((e) => {
return baaWrapper.RESTdeleteRecord(e);
export default defineEventHandler(async (e) => {
const id = e.context.params?.id as string;
try {
await database.client.delete({
where: {
id: BigInt(id),
},
});
} catch (e) {
// FIXME: should be 500 on errors other than "RecordNotFound"
throw createError({ statusCode: 404 });
}
return null;
}); });

View file

@ -1,18 +1,7 @@
import { defineEventHandler } from "h3"; /* global defineEventHandler */
import { database } from "~/server/utils/database"; import { baaWrapper } from "../clients.get";
import { prismaToWeb } from "~/server/utils/prismaToWeb";
import { createError } from "#imports"; export default defineEventHandler((e) => {
return baaWrapper.RESTgetRecord(e);
export default defineEventHandler(async (e) => {
const key = e.context.params?.id as string;
const rvalue = await database.client.findUnique({
where: {
id: BigInt(key),
},
});
if (!rvalue) throw createError({ statusCode: 404 });
return prismaToWeb(rvalue);
}); });

View file

@ -1,23 +1,8 @@
import { defineEventHandler, readBody } from "h3"; /* global defineEventHandler */
import { checkIsClient } from "../clients.post"; import { checkIsClient } from "../clients.post";
import { database } from "~/server/utils/database"; import { baaWrapper } from "../clients.get";
import { prismaToWeb } from "~/server/utils/prismaToWeb";
import { createError } from "#imports"; export default defineEventHandler((e) => {
return baaWrapper.RESTpatchRecord(e, checkIsClient);
export default defineEventHandler(async (e) => {
const body = await readBody(e);
const id = e.context.params?.id as string;
if (!checkIsClient(body, true)) throw createError({ message: "Invalid body", statusCode: 400 });
const rvalue = await database.client.update({
where: {
id: BigInt(id),
},
data: body,
});
return prismaToWeb(rvalue);
}); });

View file

@ -1,16 +1,9 @@
import { defineEventHandler } from "h3"; /* global defineEventHandler */
import { getOrders } from "~/server/api/orders.get"; import { baaWrapper } from "~/server/api/orders.get";
import { prismaToWeb } from "~/server/utils/prismaToWeb";
import getPaginatedParameters from "~/server/utils/baaPageParsing";
export default defineEventHandler((e) => { export default defineEventHandler(async (e) => {
const pageParameters = getPaginatedParameters(e, 50, 200); const baa = await baaWrapper.RESTget(e, 50, 200, "`client` = ?", [e.context.params?.id]);
const clientId = e.context.params?.id as string; console.log(baa);
return getOrders( return baa;
pageParameters,
{
clientId: BigInt(clientId),
},
).then(prismaToWeb);
}); });

View file

@ -1,9 +1,7 @@
import { defineEventHandler } from "h3"; /* global defineEventHandler */
import { database } from "~/server/utils/database"; import { baaWrapper } from "../clients.get";
export default defineEventHandler(async (e) => { export default defineEventHandler((e) => {
return { return baaWrapper.RESTrecordCount(e);
count: await database.client.count({}),
};
}); });

9
server/api/dbtest.get.ts Normal file
View file

@ -0,0 +1,9 @@
/* global defineEventHandler */
import { database } from "../utils/database";
export default defineEventHandler(async () => {
const [owo] = await database.execute("SELECT * FROM `sch_baza_smartfony`.`lombardy`");
return owo as {id: number, nazwa: string, adres: string, kontakt: string}[];
});

26
server/api/dbtest.post.ts Normal file
View file

@ -0,0 +1,26 @@
/* global defineEventHandler, readBody */
import { RowDataPacket } from "mysql2";
import { database } from "../utils/database";
import { isString } from "../utils/isString";
export default defineEventHandler(async (e) => {
const data = await readBody(e);
const nazwa = data.nazwa;
const adres = data.adres;
const kontakt = data.kontakt;
if (!isString(nazwa)) throw new Error("nazwa is not string");
if (!isString(adres)) throw new Error("adres is not string");
if (!isString(kontakt)) throw new Error("kontakt is not string");
const [inserted] = await database.query("INSERT INTO `sch_baza_smartfony`.`lombardy` (`nazwa`, `adres`, `kontakt`) VALUES (?, ?, ?);", [nazwa, adres, kontakt]) as RowDataPacket[];
return {
id: inserted.insertId as number,
nazwa,
adres,
kontakt,
};
});

View file

@ -0,0 +1,9 @@
/* global defineEventHandler */
import { database } from "~/server/utils/database";
export default defineEventHandler(async (e) => {
if (!e.context.params?.id) return Error("id is not provided");
const rowID = e.context.params.id;
await database.execute("DELETE FROM `sch_baza_smartfony`.`lombardy` WHERE `id` = ?", [rowID]);
});

View file

@ -1,4 +1,4 @@
import { defineEventHandler } from "h3"; /* global defineEventHandler */
export default defineEventHandler((event) => { export default defineEventHandler((event) => {
const message = event.node.req.read(); const message = event.node.req.read();

View file

@ -1,16 +0,0 @@
import { defineEventHandler } from "h3";
import { database } from "../utils/database";
export async function isFirstRun() {
try {
const numberOfUsers = await database.user.count();
return numberOfUsers === 0;
} catch {
// We could fall here if the database is not initialized
return true;
}
}
export default defineEventHandler((e) => {
return isFirstRun();
});

View file

@ -1,52 +0,0 @@
import url from "node:url";
import path from "node:path";
import { defineEventHandler, setResponseStatus, readBody } from "h3";
// eslint-disable-next-line import/default
import PrismaMigrate from "@prisma/migrate";
import { database } from "../utils/database";
import { isFirstRun } from "./firstRun.get";
import { getPasswordHash } from "./login.post";
import Snowflake from "~/utils/snowflake";
import { createError } from "#imports";
export default defineEventHandler(async (e) => {
if (!isFirstRun()) {
setResponseStatus(e, 404);
return null;
}
const body = await readBody(e);
if (typeof body !== "object") throw createError({ message: "Invalid body", statusCode: 400 });
const username = body.username;
if (typeof username !== "string") throw createError({ message: "username is not string", statusCode: 400 });
const password = body.password;
if (typeof password !== "string") throw createError({ message: "password is not string", statusCode: 400 });
const email = body.email;
if (typeof email !== "string") throw createError({ message: "email is not string", statusCode: 400 });
const dbPushParam = [
"--force-reset",
"--skip-generate",
];
if (!import.meta.dev ?? true) {
const mainPath = path.dirname(url.fileURLToPath(import.meta.url));
dbPushParam.push(
"--schema",
`${mainPath}/node_modules/.prisma/client/schema.prisma`,
);
}
await (new PrismaMigrate.DbPush()).parse(dbPushParam);
await database.user.create({
data: {
id: new Snowflake().state,
username,
email,
password: getPasswordHash(password),
},
});
return null;
});

View file

@ -1,4 +1,4 @@
import { defineEventHandler } from "h3"; /* global defineEventHandler */
export default defineEventHandler(() => { export default defineEventHandler(() => {
return "Hi mom!"; return "Hi mom!";

View file

@ -1,18 +1,10 @@
/* global defineEventHandler, getCookie, setCookie, readBody, createError */
import crypto from "crypto"; import crypto from "crypto";
import { defineEventHandler, getCookie, setCookie, readBody } from "h3";
import { database } from "../utils/database"; import { database, data } from "../utils/database";
import { isString } from "../utils/isString"; import { isString } from "../utils/isString";
import { cookieSettings } from "../utils/rootUtils"; import { cookieSettings } from "../utils/rootUtils";
import SessionToken from "../utils/SessionToken"; import Snowflake from "~/utils/snowflake";
import { createError } from "#imports";
export function getPasswordHash(password: string) {
return crypto.createHmac("sha512", "42")
.update(password)
.digest();
}
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {
if (getCookie(e, "token")) if (getCookie(e, "token"))
@ -26,25 +18,23 @@ export default defineEventHandler(async (e) => {
if (!isString(login)) throw createError({ statusCode: 400, message: "Login is not string." }); if (!isString(login)) throw createError({ statusCode: 400, message: "Login is not string." });
if (!isString(password)) throw createError({ statusCode: 400, message: "Password is not string." }); if (!isString(password)) throw createError({ statusCode: 400, message: "Password is not string." });
const hashedPassword = getPasswordHash(password); const hashedPassword = crypto.createHmac("sha512", "42")
.update(password)
.digest("hex");
const account = await database.user.findUnique({ const [account] = await database.query(
where: { "SELECT CONVERT(`id`, CHAR(32)) AS `id` from `users` WHERE `username` = ? AND LOWER(HEX(`password`)) = ? LIMIT 1",
username: login, [login, hashedPassword],
password: hashedPassword, )as unknown as data<{id: string}>;
},
select: {
id: true,
},
});
if (account === null) throw createError({ statusCode: 400, message: "Invalid username or password." }); if (account.length === 0) throw createError({ statusCode: 400, message: "Invalid username or password." });
const session = new SessionToken(account.id); const sessionId = new Snowflake().toString();
await database.session.create({ await database.query(
data: session.toPrisma(), "INSERT INTO `sessions` (`id`, `user`) VALUES ( ? , ? )",
}); [sessionId, account[0].id],
setCookie(e, "token", session.toString(), cookieSettings); );
return { message: "Login successful", token: session.toString() }; setCookie(e, "token", sessionId, cookieSettings);
return { message: "Login successful", token: sessionId };
}); });

View file

@ -1,11 +1,9 @@
import { defineEventHandler, getCookie, deleteCookie } from "h3"; /* global defineEventHandler, createError, getCookie, deleteCookie */
import { isAuthorised } from "../middleware/auth"; import { isAuthorised } from "../middleware/auth";
import { database } from "../utils/database"; import { database } from "../utils/database";
import { cookieSettings } from "../utils/rootUtils"; import { cookieSettings } from "../utils/rootUtils";
import { createError } from "#imports";
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {
const token = getCookie(e, "token"); const token = getCookie(e, "token");
if (token === undefined) { if (token === undefined) {
@ -23,10 +21,9 @@ export default defineEventHandler(async (e) => {
}); });
} }
database.session.delete({ database.query(
where: { "DELETE FROM `sessions` WHERE `id` = ?",
id: BigInt(token), [token],
}, );
});
return { message: "Logged out" }; return { message: "Logged out" };
}); });

View file

@ -1,65 +1,37 @@
import { defineEventHandler } from "h3"; /* global defineEventHandler */
import { type Order, type Client, Prisma } from "@prisma/client";
import getPaginatedParameters, { type pageData } from "../utils/baaPageParsing"; import BaaPagination from "../utils/baaPagination";
import { database } from "../utils/database"; import { data, database } from "../utils/database";
import { prismaToWeb } from "~/server/utils/prismaToWeb"; import { client, orderSummary } from "~/utils/types/database";
type orderSummary = Omit<Order, "clientId"> & { export const baaWrapper = new BaaPagination<orderSummary, "id">(
client: Client; "orderSummaries",
value: number; "id",
imported_products_count: number; "*, CONVERT(`client`, CHAR) AS `client`, CONVERT(`user`, CHAR) as `user`",
work_count: number; );
};
export async function getOrders( export default defineEventHandler(async (e) => {
pageParameters: pageData, const orders = await baaWrapper.RESTget(e, 50, 200);
where?: Prisma.OrderWhereInput,
) {
const data = await database.order.findPaginated(
pageParameters,
{
select: {
id: true,
client: true,
userId: true,
draft: true,
imported_products: {
select: {
price: true,
},
},
work: {
select: {
price: true,
},
},
},
where,
},
);
const rvalue = new Array<orderSummary>(); const uniqueClients: Array<string> = [];
for (const i of orders) {
for (const i of data) { if (!uniqueClients.includes(i.client))
const importedProductsPriceSum = i.imported_products.reduce((pv, cv) => pv + cv.price.toNumber(), 0); uniqueClients.push(database.escape(i.client));
const workPriceSum = i.work.reduce((pv, cv) => pv + cv.price.toNumber(), 0);
rvalue.push({
id: i.id,
client: i.client,
draft: i.draft,
imported_products_count: i.imported_products.length,
userId: i.userId,
value: importedProductsPriceSum + workPriceSum,
work_count: i.work.length,
});
} }
return rvalue; const [clients] = await database.query(
} ["SELECT",
"*,",
"CONVERT(`id`, CHAR) AS `id`",
"FROM `clients`",
"WHERE `id` IN",
`(${uniqueClients.join(', ')})`,
].join(" "),
) as data<client>;
export default defineEventHandler((e) => { const rvalue: Array<Omit<typeof orders, "client"> | { client?: client }> = [];
const pageParameters = getPaginatedParameters(e, 50, 200);
return getOrders(pageParameters, {}).then(prismaToWeb); for (const i of orders)
rvalue.push({ ...i, client: clients.find(e => i.client === e.id) });
return rvalue;
}); });

View file

@ -1,44 +1,37 @@
import { defineEventHandler, readBody, setResponseStatus } from "h3"; /* global defineEventHandler, createError, readBody, setResponseStatus */
import * as Prisma from "@prisma/client";
import { createValidationError, handleRecursedValidationError } from "../utils/validation"; import { createValidationError, handleRecursedValidationError } from "../utils/validation";
import { database } from "../utils/database"; import { database as db } from "../utils/database";
import getRequestingUser from "../utils/getRequestingUser"; import getRequestingUser from "../utils/getRequestingUser";
import { getOrder } from "./orders/[id].get"; import { getOrder } from "./orders/[id].get";
import { prismaToWeb } from "~/server/utils/prismaToWeb";
import Snowflake from "~/utils/snowflake"; import Snowflake from "~/utils/snowflake";
import { createError } from "#imports"; type importedProduct = {
type importedProduct<inOrder extends boolean = boolean> = {
orderId: inOrder extends true ? never : string,
name: string | null, name: string | null,
link: string, link: string,
price_imported: number, price_imported: number,
price: number, price: number,
} }
type work<inOrder extends boolean = boolean> = { type work = {
orderId: inOrder extends true ? never : string, offer: string,
offerId: string,
price: number, price: number,
notes: string | null, notes: string | null,
fulfilled: boolean, is_fulfilled: boolean | 0 | 1,
} }
type order = { type order = {
clientId: string, client: string,
// userId: string, // user: string,
draft: boolean, is_draft: boolean | 0 | 1,
imported_products: Array<importedProduct<true>>, imported_products: Array<importedProduct>,
work: Array<work<true>>, work: Array<work>,
}; };
export function checkIsWork<Patch extends boolean = boolean, inOrder extends boolean = boolean>( export function checkIsWork<Patch extends boolean = boolean>(
value: any, value: any,
patch: Patch, patch: Patch,
needsOrderId: inOrder, ): value is Patch extends true ? Partial<work> : work {
): value is Patch extends true ? Partial<work<inOrder>> : work<inOrder> {
const errors = new Map<string, string>(); const errors = new Map<string, string>();
if (typeof value !== "object") { if (typeof value !== "object") {
@ -48,24 +41,19 @@ export function checkIsWork<Patch extends boolean = boolean, inOrder extends boo
}); });
} }
if (!(typeof value.orderId === "string" || (patch && value.orderId === undefined) || !needsOrderId)) errors.set("orderId", "is not string"); if (!(typeof value.offer === "string" || (patch && value.offer === undefined))) errors.set("offer", "is not string");
if (!(typeof value.offerId === "string" || (patch && value.offerId === undefined))) errors.set("offerId", "is not string");
if (!(typeof value.price === "number" || (patch && value.price === undefined))) errors.set("price", "is not price"); 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.notes === "string" || value.notes === null || (patch && value.notes === undefined))) errors.set("notes", "is not string or null");
if (!(typeof value.is_fulfilled === "boolean" || (patch && value.is_fulfilled === undefined))) errors.set("is_fulfilled", "is not boolean"); 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");
// TODO: Excessive property checking
// Excessive properties should be checked and an error should be thrown if there is one
if (errors.size !== 0) throw createValidationError(errors); if (errors.size !== 0) throw createValidationError(errors);
return true; return true;
} }
export function checkIsImportedProduct<Patch extends boolean = boolean, inOrder extends boolean = boolean>( export function checkIsImportedProduct<Patch extends boolean = boolean>(
value: any, value: any,
patch: Patch, patch: Patch,
needsOrderId: inOrder, ): value is Patch extends true ? Partial<importedProduct> : importedProduct {
): value is Patch extends true ? Partial<importedProduct<inOrder>> : importedProduct<inOrder> {
const errors = new Map<string, string>(); const errors = new Map<string, string>();
if (typeof value !== "object") { if (typeof value !== "object") {
@ -75,14 +63,10 @@ export function checkIsImportedProduct<Patch extends boolean = boolean, inOrder
}); });
} }
if (!(typeof value.orderId === "string" || (patch && value.orderId === undefined) || !needsOrderId)) errors.set("orderId", "is not string");
if (!(typeof value.name === "string" || value.name === null || (patch && value.name === undefined))) errors.set("name", "is not string or null"); 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.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_imported === "number" || (patch && value.name === undefined))) errors.set("price_imported", "is not number");
if (!(typeof value.price === "number" || (patch && value.price === undefined))) errors.set("price", "is not number"); if (!(typeof value.price || (patch && value.price === undefined))) errors.set("price", "is not number");
// TODO: Excessive property checking
// Excessive properties should be checked and an error should be thrown if there is one
if (errors.size !== 0) throw createValidationError(errors); if (errors.size !== 0) throw createValidationError(errors);
@ -92,7 +76,7 @@ export function checkIsImportedProduct<Patch extends boolean = boolean, inOrder
export function checkIsOrder<Patch extends boolean = boolean>( export function checkIsOrder<Patch extends boolean = boolean>(
value: any, value: any,
patch: Patch, patch: Patch,
): value is Patch extends true ? Partial<Omit<order, "imported_products" | "work">> : order { ): value is Patch extends true ? Partial<Pick<order, "client" | "is_draft">> : order {
const errors = new Map<string, string>(); const errors = new Map<string, string>();
if (typeof value !== "object") { if (typeof value !== "object") {
@ -102,22 +86,19 @@ export function checkIsOrder<Patch extends boolean = boolean>(
}); });
} }
if (!(typeof value.clientId === "string" || (patch && value.clientId === undefined))) errors.set("clientId", "is not string"); if (!(typeof value.client === "string" || (patch && value.client === undefined))) errors.set("client", "is not string");
if (!(typeof value.draft === "boolean" || (patch && value.is_draft === undefined))) errors.set("draft", "is not boolean"); 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"); 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"); 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"); if (!(value.work instanceof Array)) errors.set("work", "is not array");
else if (patch && value.work !== undefined) errors.set("work", "cannot patch from order"); else if (patch && value.work !== undefined) errors.set("work", "cannot patch from order");
// TODO: Excessive property checking
// Excessive properties should be checked and an error should be thrown if there is one
if (!patch) { if (!patch) {
const importedProducts = value.imported_products; const importedProducts = value.imported_products;
if (importedProducts instanceof Array) { if (importedProducts instanceof Array) {
for (const i in importedProducts) { for (const i in importedProducts) {
try { try {
checkIsImportedProduct(importedProducts[i], patch, false); checkIsImportedProduct(importedProducts[i], patch);
} catch (e) { } catch (e) {
handleRecursedValidationError(e, errors, `imported_products[${i}]`); handleRecursedValidationError(e, errors, `imported_products[${i}]`);
} }
@ -128,7 +109,7 @@ export function checkIsOrder<Patch extends boolean = boolean>(
if (work instanceof Array) { if (work instanceof Array) {
for (const i in work) { for (const i in work) {
try { try {
checkIsWork(work[i], patch, false); checkIsWork(work[i], patch);
} catch (e) { } catch (e) {
handleRecursedValidationError(e, errors, `work[${i}]`); handleRecursedValidationError(e, errors, `work[${i}]`);
} }
@ -143,49 +124,49 @@ export function checkIsOrder<Patch extends boolean = boolean>(
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {
const body = await readBody(e); const body = await readBody(e);
const id = new Snowflake().state; const id = new Snowflake().toString();
const user = await getRequestingUser(e); const user = await getRequestingUser(e);
if (!checkIsOrder(body, false)) throw createError({ message: "Invalid body", statusCode: 400 }); if (!checkIsOrder(body, false)) throw createError({ message: "Invalid body", statusCode: 400 });
await database.order.create({ const database = await db.new();
data: { await database.beginTransaction();
clientId: BigInt(body.clientId),
draft: body.draft, await database.query(
imported_products: { ["INSERT INTO",
createMany: { "`orders`",
data: body.imported_products.reduce( "VALUES",
(pV, cV) => { "(?, ?, ?, ?)",
pV.push({ ].join(" "),
...cV, [id, body.client, user.id, body.is_draft],
id: new Snowflake().state, );
});
return pV; const promises: Array<Promise<any>> = [];
}, for (const i of body.imported_products) {
[] as Array<Omit<Prisma.Prisma.ImportedProductCreateManyOrderInput, "orderId">>, promises.push(database.query(
), ["INSERT INTO",
}, "`imported_products`",
}, "VALUES",
work: { "(?, ?, ?, ?, ?, ?)",
createMany: { ].join(" "),
data: body.work.reduce( [new Snowflake().toString(), id, i.name, i.link, i.price_imported, i.price],
(pV, cV) => { ));
pV.push({ }
...cV,
id: new Snowflake().state, for (const i of body.work) {
offerId: BigInt(cV.offerId), promises.push(database.query(
}); ["INSERT INTO",
return pV; "`work`",
}, "VALUES",
[] as Array<Omit<Prisma.Prisma.WorkCreateManyOrderInput, "orderId">>, "(?, ?, ?, ?, ?, ?)",
), ].join(" "),
}, [new Snowflake().toString(), id, i.offer, i.price, i.notes, i.is_fulfilled],
}, ));
id, }
userId: user.id,
}, await Promise.all(promises);
}); await database.commit();
setResponseStatus(e, 201); setResponseStatus(e, 201);
return getOrder(id).then(prismaToWeb); return getOrder(id);
}); });

View file

@ -1,22 +1,17 @@
import { defineEventHandler } from "h3"; /* global defineEventHandler, createError */
import { ResultSetHeader } from "mysql2";
import { database } from "~/server/utils/database"; import { database } from "~/server/utils/database";
import { createError } from "#imports";
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {
const id = e.context.params?.id as string; const id = e.context.params?.id;
try { const [result] = await database.query(
await database.order.delete({ "DELETE FROM `orders` WHERE `id` = ?",
where: { [id],
id: BigInt(id), ) as unknown as [ResultSetHeader];
},
}); if (result.affectedRows === 0) throw createError({ statusCode: 404 });
} catch (e) {
// FIXME: should be 500 on errors other than "RecordNotFound"
throw createError({ statusCode: 404 });
}
return null; return null;
}); });

View file

@ -1,39 +1,108 @@
import { defineEventHandler } from "h3"; /* global defineEventHandler, createError */
import { createError } from "#imports";
import { database } from "~/server/utils/database"; import { offer as offerType, order } from "~/utils/types/database";
import { prismaToWeb } from "~/server/utils/prismaToWeb"; import { database, data } from "~/server/utils/database";
export async function orderExists(id: bigint) { export async function orderExists(id: string) {
const exists = await database.order.findUnique({ const [[exists]] = await database.query(
where: { "SELECT EXISTS(*) AS `exists` FROM `orders` WHERE `id` = ?",
id, [id],
}, ) as data<{exists: 0 | 1}>;
});
return exists !== null; return exists.exists === 1;
} }
export async function getOrder(id: bigint) { export async function getImportedProducts(id: string) {
const order = await database.order.findUnique({ const [importedProducts] = await database.query(
where: { ["SELECT",
id, "CONVERT(`id`, CHAR) AS `id`,",
}, "`name`,",
include: { "`link`,",
imported_products: true, "`price`,",
work: { "`price_imported`",
include: { "FROM `imported_products`",
offer: true, "WHERE `order` = ?",
}, ].join(" "),
}, [id],
client: true, ) 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 }); if (!order) throw createError({ statusCode: 404 });
return order;
const importedProducts = await getImportedProducts(id);
const work = await getWork(id);
return { ...order, imported_products: importedProducts, work };
} }
export default defineEventHandler((e) => { export default defineEventHandler((e) => {
const key = e.context.params?.id as string; const key = e.context.params?.id;
return getOrder(BigInt(key)).then(prismaToWeb); return getOrder(key as string);
}); });

View file

@ -1,27 +1,17 @@
import { defineEventHandler, readBody } from "h3"; /* global defineEventHandler, readBody, createError */
import { checkIsOrder } from "../orders.post"; import { checkIsOrder } from "../orders.post";
import { getOrder } from "./[id].get"; import { database as db } from "~/server/utils/database";
import { database } from "~/server/utils/database";
import { prismaToWeb } from "~/server/utils/prismaToWeb";
import { createError } from "#imports";
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {
const body = await readBody(e); const body = await readBody(e);
const id = e.context.params?.id as string; const id = e.context.params?.id;
if (!checkIsOrder(body, true)) throw createError({ message: "Invalid body", statusCode: 400 }); if (!checkIsOrder(e, true)) throw createError({ message: "Invalid body", statusCode: 400 });
await database.order.update({ const database = await db.new();
where: { await database.beginTransaction();
id: BigInt(id),
},
data: {
clientId: body.clientId ? BigInt(body.clientId) : undefined,
draft: body.draft,
},
});
return getOrder(BigInt(id)).then(prismaToWeb); for (const [k, v] of Object.entries(body))
database.query(`UPDATE TABLE \`orders\` SET \`${k}\` = ? WHERE \`id\` = ?`, [v, id]);
}); });

View file

@ -1,27 +1,12 @@
import { defineEventHandler } from "h3"; /* global defineEventHandler, createError */
import { orderExists } from "../[id].get"; import { orderExists, getImportedProducts } from "../[id].get";
import { database } from "~/server/utils/database";
import { prismaToWeb } from "~/server/utils/prismaToWeb";
import { createError } from "#imports";
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {
const orderId = e.context.params?.id as string; const id = e.context.params?.id as string;
if (!(await orderExists(BigInt(orderId)))) throw createError({ statusCode: 404 }); if (!orderExists(id)) throw createError({ statusCode: 404 });
return database.importedProduct.findMany({ const importedProducts = await getImportedProducts(id);
where: { return importedProducts;
orderId: BigInt(orderId),
},
select: {
id: true,
link: true,
name: true,
orderId: true,
price: true,
price_imported: true,
},
}).then(prismaToWeb);
}); });

View file

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

View file

@ -1,31 +1,12 @@
import { defineEventHandler } from "h3"; /* global defineEventHandler, createError */
import { orderExists } from "../[id].get"; import { orderExists, getWork } from "../[id].get";
import { database } from "~/server/utils/database";
import { prismaToWeb } from "~/server/utils/prismaToWeb";
import { createError } from "#imports";
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {
const orderId = e.context.params?.id as string; const id = e.context.params?.id as string;
if (!await orderExists(BigInt(orderId))) throw createError({ statusCode: 404 }); if (!orderExists(id)) throw createError({ statusCode: 404 });
const data = await database.work.findMany({ const work = await getWork(id);
where: { return work;
orderId: BigInt(orderId),
},
select: {
id: true,
fulfilled: true,
notes: true,
offer: true,
orderId: true,
price: true,
},
});
if (!data) throw createError({ statusCode: 404 });
return prismaToWeb(data);
}); });

View file

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

View file

@ -1,24 +1,20 @@
import { defineEventHandler } from "h3"; /* global defineEventHandler, createError */
import { ResultSetHeader } from "mysql2";
import { orderExists } from "../../[id].get";
import { database } from "~/server/utils/database"; import { database } from "~/server/utils/database";
import { createError } from "#imports";
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {
const idOrder = e.context.params?.id as string; const idOrder = e.context.params?.id as string;
const idWork = e.context.params?.idWork as string; const idWork = e.context.params?.idWork as string;
try { if (!orderExists(idOrder)) throw createError({ statusCode: 404 });
await database.work.delete({
where: {
id: BigInt(idWork),
orderId: BigInt(idOrder),
},
});
} catch (e) {
// FIXME: should be 500 on errors other than "RecordNotFound"
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; return null;
}); });

View file

@ -1,32 +1,11 @@
import { defineEventHandler } from "h3"; /* global defineEventHandler, createError */
import { orderExists } from "../../[id].get"; import { orderExists, getWork } from "../../[id].get";
import { database } from "~/server/utils/database";
import { prismaToWeb } from "~/server/utils/prismaToWeb";
import { createError } from "#imports"; export default defineEventHandler((e) => {
export default defineEventHandler(async (e) => {
const idOrder = e.context.params?.id as string; const idOrder = e.context.params?.id as string;
const idWork = e.context.params?.idWork as string; const idWork = e.context.params?.idWork as string;
if (!await orderExists(BigInt(idOrder))) throw createError({ statusCode: 404 }); if (!orderExists(idOrder)) throw createError({ statusCode: 404 });
const data = await database.work.findUnique({ return getWork(idWork);
where: {
orderId: BigInt(idOrder),
id: BigInt(idWork),
},
select: {
id: true,
fulfilled: true,
notes: true,
offer: true,
orderId: true,
price: true,
},
});
if (!data) throw createError({ statusCode: 404 });
return prismaToWeb(data);
}); });

View file

@ -1,9 +0,0 @@
import { defineEventHandler } from "h3";
import { database } from "~/server/utils/database";
export default defineEventHandler(async (e) => {
return {
count: await database.order.count({}),
};
});

View file

@ -1,8 +1,14 @@
import { defineEventHandler } from "h3"; /* global defineEventHandler, getCookie */
import getRequestingUser from "~/server/utils/getRequestingUser"; import { database, data } from "~/server/utils/database";
import { prismaToWeb } from "~/server/utils/prismaToWeb"; import { user } from "~/utils/types/database";
export default defineEventHandler((e) => { export default defineEventHandler(async (e) => {
return getRequestingUser(e).then(prismaToWeb); const token = getCookie(e, "token");
const [[userData]] = await database.query(
"SELECT CONVERT(`users`.`id`, CHAR(32)) as `id`, `users`.`username` as `username`, `users`.`email` as `email`, `users`.`display_name` as `display_name` FROM `sessions` LEFT JOIN `users` ON `sessions`.`user` = `users`.`id` WHERE `sessions`.`id` = ?",
[token],
) as unknown as data<user>;
return userData;
}); });

View file

@ -1,10 +1,6 @@
import { defineEventHandler, getCookie } from "h3"; /* global defineEventHandler, createError, getCookie */
import SessionToken from "../utils/SessionToken";
import { database } from "~/server/utils/database"; import { database, data } from "~/server/utils/database";
import getRequestingUser from "~/server/utils/getRequestingUser";
import { createError } from "#imports";
const endpointsWithoutAuth: string[] = [ const endpointsWithoutAuth: string[] = [
"/dbtest", "/dbtest",
@ -12,7 +8,6 @@ const endpointsWithoutAuth: string[] = [
"/hi", "/hi",
"/login", "/login",
"/logout", "/logout",
"/firstRun",
]; ];
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {
@ -36,18 +31,10 @@ export default defineEventHandler(async (e) => {
*/ */
export async function isAuthorised(token: string | undefined): Promise<boolean> { export async function isAuthorised(token: string | undefined): Promise<boolean> {
if (!token) return false; if (!token) return false;
try { const [[session]] = await database.query(
await database.session.findUniqueOrThrow({ "SELECT EXISTS(SELECT `id` FROM `sessions` WHERE `id` = ? AND `expiry_date` >= NOW()) as `logged_in`",
where: { [token],
...SessionToken.fromString(token).toPrisma(), ) as unknown as data<{logged_in: number}>;
expiry_date: {
gte: new Date(),
},
},
});
return true; return session.logged_in === 1;
} catch (e) {
return false;
}
} }

View file

@ -1,49 +0,0 @@
import crypto from "node:crypto";
import { type Session } from "@prisma/client";
import Snowflake from "~/utils/snowflake";
/** Represents a Session token, without expiry data. */
export default class SessionToken {
userId: bigint;
sessionId: bigint;
sessionToken: Buffer;
constructor(userId: bigint, sessionId?: bigint, sessionToken?: Buffer) {
this.userId = userId;
this.sessionId = sessionId ?? new Snowflake().state;
this.sessionToken = sessionToken ?? crypto.randomBytes(64);
}
/** Creates SessionToken from a string.
* @param string The strinct to create from.
* @returns The SessionToken object.
*/
static fromString(string: string): SessionToken {
const parameters = string.split(".");
return new SessionToken(
Buffer.from(parameters[0], "base64").readBigUInt64LE(),
Buffer.from(parameters[1], "base64").readBigUInt64LE(),
Buffer.from(parameters[2], "base64"),
);
}
toString(): string {
const stringUserId = Buffer.copyBytesFrom(new BigUint64Array([this.userId])).toString("base64");
const stringSessionId = Buffer.copyBytesFrom(new BigUint64Array([this.sessionId])).toString("base64");
const stringSessionToken = this.sessionToken.toString("base64");
return `${stringUserId}.${stringSessionId}.${stringSessionToken}`;
}
/** Returns this SessionToken as Prisma object.
* For use in where parameter.
* @returns this as prisma object.
*/
toPrisma(): Omit<Session, "expiry_date"> {
return {
id: this.sessionId,
userId: this.userId,
sessionToken: this.sessionToken,
};
}
}

View file

@ -1,108 +0,0 @@
// BAA stands for Before Around After
import { getQuery, type H3Event } from "h3";
import { type QueryObject } from "ufo";
import { isString } from "./isString";
import { createError } from "#imports";
type queryType<none extends boolean = boolean> = none extends false ? {
type: "before" | "after" | "around",
id: bigint
} : {
type: null
};
export type pageData<none extends boolean = boolean> = queryType<none> & { count: number }
/**
* Gets queryType for a given query with a value
* @param query the query to parse
* @throws if query malformed (multiple before/after/around)
*/
function getLocationParameterType(query: QueryObject): queryType {
const before = query.before;
const after = query.after;
const around = query.around;
let setLocationParametersCount = 0;
let rvalue: queryType = { type: null };
if (isString(before)) {
setLocationParametersCount++;
rvalue = { type: "before", id: BigInt(before) };
}
if (isString(after)) {
setLocationParametersCount++;
rvalue = { type: "after", id: BigInt(after) };
}
if (isString(around)) {
setLocationParametersCount++;
rvalue = { type: "around", id: BigInt(around) };
}
if (setLocationParametersCount > 1) {
throw createError({
statusCode: 400,
message: "multiple location parameters not allowed",
});
}
return rvalue;
}
/** Gets the count parameter from the query object.
* @param query the query to check.
* @param defaultCount the default count if the query doesn't have count parameter. (default 50)
* @param countLimit the maximum count of the parameter before throwing. (default 200)
* @returns the value of count parameter.
* @throws if the parameter in query exceeds provided countLimit.
*/
function getRequestedCount(
query: QueryObject,
defaultCount = 50,
countLimit = 200,
) {
let count = defaultCount;
if (query.limit) count = Number(query.limit);
if (count > countLimit) {
throw createError({
statusCode: 400,
message: `Cannot retrieve more than ${countLimit} records`,
});
}
if (count <= 0) {
throw createError({
statusCode: 400,
message: "Tried to retireve 0 or less records",
});
}
return count;
}
/** Gets the baa page parameters from the H3event,
* @param e the H3event to fetch parameters.
* @param defaultCount the default count to use if there is no count parameter. (default 50)
* @param countLimit the maximum value of the count parameter before throwing an error. (default 200)
* @returns the page data found in the query.
* @throws if event has a count parameter in the query that exceed provided countLimit.
*/
export default function getPaginatedParameters(
e: H3Event,
defaultCount = 50,
countLimit = 200,
): pageData {
const query = getQuery(e);
const queryParameters = getLocationParameterType(query);
const queryCount = getRequestedCount(query, defaultCount, countLimit);
return {
...queryParameters,
count: queryCount,
};
}

View file

@ -0,0 +1,261 @@
/* global defineEventHandler, getQuery, createError, readBody, setResponseStatus */
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 { client } from "~/utils/types/database";
type queryType = {
type: "before" | "after" | "around",
id: string
} | {
type: null
};
/**
* Before, around, after pagination wrapper
*/
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
* @param query the query to parse
* @throws if query malformed (multiple before/after/around)
*/
static getLocationParameterType(query: QueryObject): queryType {
const before = query.before;
const after = query.after;
const around = query.around;
let setLocationParametersCount = 0;
let rvalue: queryType = { type: null };
if (isString(before)) {
setLocationParametersCount++;
rvalue = { type: "before", id: before };
}
if (isString(after)) {
setLocationParametersCount++;
rvalue = { type: "after", id: after };
}
if (isString(around)) {
setLocationParametersCount++;
rvalue = { type: "around", id: around };
}
if (setLocationParametersCount > 1) {
throw createError({
statusCode: 400,
message: "multiple location parameters not allowed",
});
}
return rvalue;
}
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],
) 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],
) 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` +
"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)],
) 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],
) as unknown as data<T>;
return data;
}
default:
throw createError("Not implemented");
}
}
RESTget(
e: H3Event,
defaultLimit = 50,
limitLimit = 200,
where = "",
bind: Array<any> = [],
) {
const query = getQuery(e);
let limit = defaultLimit;
if (query.limit) limit = Number(query.limit);
if (limit > limitLimit) {
throw createError({
statusCode: 400,
message: `Cannot retrieve more than ${limitLimit} records`,
});
}
if (limit <= 0) {
throw createError({
statusCode: 400,
message: "Tried to retireve 0 or less records",
});
}
const queryData = BaaPagination.getLocationParameterType(query);
return this.getPagedResults(queryData, limit, where, bind);
}
async RESTpost<K extends keyof Omit<T, keyType>>(
e: H3Event,
fields: Array<K>,
valueChecker: (obj: unknown) => obj is {[P in K]: T[P]},
) {
const body = await readBody(e);
const id = new Snowflake().toString();
if (!valueChecker(body)) throw createError({ message: "Invalid body", statusCode: 400 });
const arrayToInsert: Array<any> = [id];
arrayToInsert.push(...fields.map(field => body[field]));
await database.query(
`INSERT INTO \`${this.table}\` ` +
`(\`${this.key}\`,\`${fields.join("`, `")}\`) ` +
"VALUES (" +
"?, ".repeat(fields.length) +
"?)",
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
return { id, ...body };
}
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}\` = ?`,
[key],
) as data<T>;
if (!data[0]) {
throw createError({
statusCode: 404,
});
}
return data[0];
}
async RESTpatchRecord(
e: H3Event,
valueChecker: (obj: unknown) => obj is Partial<Omit<T, keyType>>,
) {
const body = await readBody(e);
const key = e.context.params?.[this.key];
if (!valueChecker(body)) throw createError({ message: "Invalid body", statusCode: 400 });
for (const [k, v] of Object.entries(body)) {
// FIXME: use single database.query method instead of looping through keys and values
const [res] = await database.query(
// I believe it is safe to put key (k) in the template
// because it is limited to 4 values here
`UPDATE \`${this.table}\` SET \`${k}\` = ? WHERE \`${this.key}\` = ?`,
[v, key],
) as unknown as [ResultSetHeader];
if (res.affectedRows !== 1) {
throw createError({
statusCode: 404,
});
}
}
const [data] = await database.query(
`SELECT ${this.select}, CONVERT(\`${this.key}\`, CHAR) AS \`${this.key}\` FROM \`${this.table}\` WHERE \`${this.key}\` = ?`,
[key],
) as data<T>;
return data[0];
}
async RESTdeleteRecord(e: H3Event) {
const key = e.context.params?.[this.key];
const [result] = await database.query(
`DELETE FROM \`${this.table}\` WHERE \`${this.key}\` = ?`,
[key],
) as unknown as [ResultSetHeader];
if (result.affectedRows === 0) throw createError({ statusCode: 404 });
return null;
}
async RESTrecordCount(
e :H3Event,
where = "",
bind: Array<any> = [],
) {
const sqlwhere = where !== "" ? `WHERE ${where}` : "";
const [[data]] = await database.query(
`SELECT COUNT(*) as \`count\` FROM \`${this.table}\` ${sqlwhere} ${this.sqlGroupBy}`,
bind,
) as data<{count: number}>;
if (!data) throw createError("Database returned no rows");
return data;
}
constructor(
table: string,
key: keyType,
select = "*",
groupBy = "",
) {
this.table = table;
this.key = key;
this.select = select;
this.groupBy = groupBy;
}
}

View file

@ -1,102 +1,17 @@
import { PrismaClient, Prisma } from "@prisma/client"; import mysql, { Connection } from "mysql2/promise";
import * as PrismaEngines from "@prisma/engines";
import { type pageData } from "./baaPageParsing"; const connectionOptions: mysql.ConnectionOptions = {
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,
};
type model = PrismaClient[Uncapitalize<Prisma.ModelName>]; export const database =
await mysql.createConnection(connectionOptions) as Connection & { new: () => Promise<Connection> };
database.new = () => { return mysql.createConnection(connectionOptions); };
PrismaEngines.ensureBinariesExist(); export type data<T> = [T[], mysql.FieldPacket[]];
function getBeforeParameters<T, A>(
pageData: pageData<false>,
fetchArgs: Prisma.Args<T, "findMany">,
) {
const _fetchArgs = Object.assign({}, fetchArgs);
return Object.assign(_fetchArgs, {
take: pageData.count,
orderBy: [
{ id: "desc" },
],
where: {
AND: [
{
id: {
_lt: pageData.id,
},
},
fetchArgs.where,
],
},
});
}
function getAfterParameters<T>(
pageData: pageData<false>,
fetchArgs: Prisma.Args<T, "findMany">,
) {
const _fetchArgs = Object.assign({}, fetchArgs);
return Object.assign(_fetchArgs, {
take: pageData.count,
orderBy: [
{ id: "desc" },
],
where: {
AND: [
{
id: {
_gt: pageData.id,
},
},
fetchArgs.where,
],
},
});
}
function getNullParameters<T>(
pageData: pageData<true>,
fetchArgs: Prisma.Args<T, "findMany">,
) {
const _fetchArgs = Object.assign({}, fetchArgs);
return Object.assign(_fetchArgs, {
take: pageData.count,
orderBy: [
{ id: "desc" },
],
});
}
export const database = new PrismaClient().$extends({
model: {
$allModels: {
findPaginated<T, A>(
this: T,
pageData: pageData,
fetchArgs: Prisma.Exact<A, Prisma.Args<T, "findMany">>,
): Promise<Prisma.Result<T, A, "findMany">> {
const context = Prisma.getExtensionContext(this) as any;
switch (pageData.type) {
case "before":
return context.findMany(getBeforeParameters(pageData, fetchArgs));
case "after":
return context.findMany(getAfterParameters(pageData, fetchArgs));
case "around":
return Promise.all([
context.findMany(getBeforeParameters({
type: "before",
id: pageData.id,
count: Math.ceil(pageData.count),
}, fetchArgs)),
context.findMany(getAfterParameters({
type: "after",
id: pageData.id,
count: Math.floor(pageData.count),
}, fetchArgs)),
]).then(rv => rv.flat()) as Promise<any>;
case null:
return context.findMany(getNullParameters(pageData, fetchArgs));
}
},
},
},
});

View file

@ -1,34 +1,24 @@
import { getCookie, H3Event } from "h3"; /* global getCookie, createError */
import { H3Event } from "h3";
import { database } from "./database"; import { database, data } from "./database";
import SessionToken from "./SessionToken"; import { user } from "~/utils/types/database";
import { createError } from "#imports";
export default async function getRequestingUser(e: H3Event) { export default async function getRequestingUser(e: H3Event) {
const cookie = getCookie(e, "token"); const cookie = getCookie(e, "token");
if (!cookie) throw createError("User not found"); const [[user]] = await database.query(
const { user } = await database.session.findUnique({ ["SELECT",
where: { "CONVERT(`users`.`id`, CHAR) as `id`,",
...SessionToken.fromString(cookie).toPrisma(), "`users`.`username`,",
expiry_date: { "`users`.`email`,",
gte: new Date(), "`users`.`display_name`",
}, "FROM",
}, "`sessions`",
select: { "LEFT JOIN `users` ON `sessions`.`user` = `users`.`id`",
user: { "WHERE `sessions`.`id` = ?",
select: { ].join(" "),
display_name: true, [cookie],
email: true, ) as data<user>;
id: true,
username: true,
},
},
},
}).then((e) => {
if (e === null) throw createError("User not found");
return e;
});
if (!user) throw createError("User not found"); if (!user) throw createError("User not found");
return user; return user;

View file

@ -1,44 +0,0 @@
import { Prisma } from "@prisma/client";
type func = (...args: any[]) => any | Function;
export type replaceJsonUnparsableToString<T> =
T extends Array<infer E> ? Array<replaceJsonUnparsableToString<E>>
: {
[K in keyof T]:
T[K] extends null ? null
: T[K] extends func ? never
: T[K] extends Prisma.Decimal ? `${number}`
: T[K] extends Array<infer E> ? Array<replaceJsonUnparsableToString<E>>
: T[K] extends object ? replaceJsonUnparsableToString<T[K]>
: T[K] extends bigint ? `${bigint}`
: T[K]
};
type exactToInterface = (...args: any[]) => any extends Function ? true : false;
function arrayPrismaToWeb<T>(array: Array<T>) {
return array.reduce(
(pV, cV) => {
pV.push(prismaToWeb(cV));
return pV;
},
[] as Array<replaceJsonUnparsableToString<T>>,
);
}
export function prismaToWeb<T>(ivalue: T): replaceJsonUnparsableToString<T> {
const rvalue: any = ivalue instanceof Array ? [] : {};
for (const i in ivalue) {
const current = ivalue[i];
if (current === null) rvalue[i] = null;
else if (typeof current === 'function') continue;
else if (current instanceof Prisma.Decimal) rvalue[i] = current.toString();
else if (current instanceof Array) rvalue[i] = arrayPrismaToWeb(current);
else if (typeof current === 'object') rvalue[i] = prismaToWeb(current);
else if (typeof current === 'bigint') rvalue[i] = current.toString();
else rvalue[i] = current;
}
return rvalue;
}

View file

@ -1,4 +1,4 @@
import { createError } from "#imports"; /* global createError */
export function createValidationError(errors: Map<string, string>) { export function createValidationError(errors: Map<string, string>) {
let message = "Invalid parameters: "; let message = "Invalid parameters: ";

View file

@ -1,87 +0,0 @@
{ pkgs ? import <nixpkgs> {} }:
let
# Updating this package will force an update for nodePackages.prisma. The
# version of prisma-engines and nodePackages.prisma must be the same for them to
# function correctly.
prisma-version = "5.7.1";
prisma-src = pkgs.fetchFromGitHub {
owner = "prisma";
repo = "prisma-engines";
rev = prisma-version;
hash = "sha256-EOYbWUgoc/9uUtuocfWDh0elExzL0+wb4PsihgMbsWs=";
};
new-prisma-engines = pkgs.rustPlatform.buildRustPackage {
pname = "prisma-engines";
version = prisma-version;
src = builtins.storePath prisma-src;
# Use system openssl.
OPENSSL_NO_VENDOR = 1;
nativeBuildInputs = [ pkgs.pkg-config pkgs.git ];
buildInputs = [
pkgs.openssl
pkgs.protobuf
];
cargoLock = {
lockFile = "${prisma-src}/Cargo.lock";
outputHashes = {
"cuid-1.3.2" = "sha256-ZihFrLerEIOdbJggaBbByRbC1sZRvF4M0LN2albB7vA=";
"barrel-0.6.6-alpha.0" = "sha256-USh0lQ1z+3Spgc69bRFySUzhuY79qprLlEExTmYWFN8=";
"graphql-parser-0.3.0" = "sha256-0ZAsj2mW6fCLhwTETucjbu4rPNzfbNiHu2wVTBlTNe4=";
"mysql_async-0.31.3" = "sha256-QIO9s0Upc0/1W7ux1RNJNGKqzO4gB4gMV3NoakAbxkQ=";
"postgres-native-tls-0.5.0" = "sha256-UYPsxhCkXXWk8yPbqjNS0illwjS5mVm3Z/jFwpVwqfw=";
};
};
preBuild = ''
export OPENSSL_DIR=${pkgs.lib.getDev pkgs.openssl}
export OPENSSL_LIB_DIR=${pkgs.lib.getLib pkgs.openssl}/lib
export PROTOC=${pkgs.protobuf}/bin/protoc
export PROTOC_INCLUDE="${pkgs.protobuf}/include";
export SQLITE_MAX_VARIABLE_NUMBER=250000
export SQLITE_MAX_EXPR_DEPTH=10000
'';
cargoBuildFlags = [
"-p" "query-engine"
"-p" "query-engine-node-api"
"-p" "schema-engine-cli"
"-p" "prisma-fmt"
];
postInstall = ''
mv $out/lib/libquery_engine${pkgs.stdenv.hostPlatform.extensions.sharedLibrary} $out/lib/libquery_engine.node
'';
# Tests are long to compile
doCheck = false;
# meta = with lib; {
# description = "A collection of engines that power the core stack for Prisma";
# homepage = "https://www.prisma.io/";
# license = licenses.asl20;
# platforms = platforms.unix;
# maintainers = with maintainers; [ pimeys tomhoule ivan aqrln ];
# };
};
in
pkgs.mkShell {
nativeBuildInputs = [
new-prisma-engines
pkgs.nodejs_18
pkgs.openssl
];
shellHook = ''
export PRISMA_SCHEMA_ENGINE_BINARY="${new-prisma-engines}/bin/schema-engine"
export PRISMA_QUERY_ENGINE_BINARY="${new-prisma-engines}/bin/query-engine"
export PRISMA_QUERY_ENGINE_LIBRARY="${new-prisma-engines}/lib/libquery_engine.node"
export PRISMA_FMT_BINARY="${new-prisma-engines}/bin/prisma-fmt"
'';
}

View file

@ -1,4 +1,4 @@
import { type CookieSerializeOptions } from "cookie-es"; import { CookieSerializeOptions } from "cookie-es";
export const cookieSettings: CookieSerializeOptions = { export const cookieSettings: CookieSerializeOptions = {
sameSite: "lax", sameSite: "lax",

99
utils/types/database.ts Normal file
View file

@ -0,0 +1,99 @@
export interface client {
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;
}
export interface session {
id: string;
user: string;
expiry_date: string;
}
export interface imported_product {
id: string;
// order: string,
name?: string;
link: string;
price_imported: string;
price: string;
}
export interface offer {
id: string;
name: string;
description?: string;
recommended_price?: string;
}
export interface work {
id: string;
// order: string,
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;
}
export interface work_template {
id: string;
// order_template: string,
offer: string | offer;
price: string;
notes?: string;
}
export interface order_template {
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}`;