forked from Wroclaw/WorkshopTasker
Compare commits
40 commits
Author | SHA1 | Date | |
---|---|---|---|
54907d6eec | |||
bcace5cbd7 | |||
cf29c26abc | |||
ac49090615 | |||
187e432182 | |||
f11b4c9a75 | |||
eabb7b89c7 | |||
015b66706f | |||
f308ab80c9 | |||
8bfc059b66 | |||
a74e148da4 | |||
ed739e27fd | |||
8bd41f8df3 | |||
b12e91ed13 | |||
0151a6c713 | |||
5d1fc30601 | |||
3101858eed | |||
f7519f32b3 | |||
10ff342991 | |||
1d8220d92c | |||
ebf5690519 | |||
434ae5843e | |||
e509bb22c1 | |||
500a9ad595 | |||
f5b205f90c | |||
ccbbfd1d3b | |||
5602c22861 | |||
95137acbed | |||
d5d2f2e889 | |||
f26206de87 | |||
aa645e710e | |||
eebf25198d | |||
be1e3909b6 | |||
1d893c4a78 | |||
267a83d484 | |||
4720202d8a | |||
90932a49c8 | |||
cbfc4e9317 | |||
75f809051c | |||
bbe0c91d7e |
63 changed files with 7897 additions and 4647 deletions
32
.forgejo/workflows/build.yml
Normal file
32
.forgejo/workflows/build.yml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
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
|
26
README.md
26
README.md
|
@ -6,18 +6,26 @@ Allows for managing order tasks and imported products for a small workshop.
|
||||||
|
|
||||||
# Running
|
# Running
|
||||||
|
|
||||||
The project was tested with the nodejs version v18.16.0. It may not work with newer versions of nodejs.
|
The project was tested with the nodejs version v18.17.1. It may not work with versions higher than 18 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_HOST` | Database host |
|
| `DB_URL` | Database url, see [this](https://www.prisma.io/docs/concepts/database-connectors/mysql#connection-url) |
|
||||||
| `DB_PORT` | Database port |
|
|
||||||
| `DB_USER` | Database user |
|
|
||||||
| `DB_PASSWORD` | Database password |
|
|
||||||
| `DB_SCHEMA` | Database schema |
|
|
||||||
|
|
||||||
After setting variables, you can run the project using `npx nuxi dev` or `npx nuxi preview`.
|
## From Repository
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
|
@ -1,20 +1,29 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
|
||||||
type optionalMap<Optional> = Optional extends true ? undefined : string | number;
|
import FormClient from "~/components/formClient.vue";
|
||||||
// type typeMap<Type extends string = {};
|
|
||||||
|
|
||||||
export type fieldDefinition<Optional extends boolean = boolean> = {
|
type optionalMap<Optional, type> = Optional extends true ? undefined | type : type;
|
||||||
|
|
||||||
|
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: "text" | "number",
|
type: type,
|
||||||
optional?: Optional,
|
optional?: Optional,
|
||||||
value?: optionalMap<Optional>,
|
value?: optionalMap<Optional, typeMap[type]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
@ -23,7 +32,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 | undefined}>({});
|
const modelValue = ref<{[key: string]: string | number | boolean | 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;
|
||||||
|
@ -40,12 +49,43 @@ emit("update:modelValue", modelValue.value);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div v-for="i of fields" :key="i.key">
|
||||||
<VTextField
|
<VTextField
|
||||||
v-for="i of fields"
|
v-if="i.type == 'text'"
|
||||||
:key="i.key"
|
|
||||||
:model-value="modelValue[i.key]"
|
:model-value="modelValue[i.key]"
|
||||||
:label="i.label"
|
:label="i.label"
|
||||||
:type="i.type"
|
type="text"
|
||||||
@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>
|
||||||
|
|
37
components/formClient.vue
Normal file
37
components/formClient.vue
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<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>
|
|
@ -1,10 +1,22 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useDisplay } from "vuetify/lib/framework.mjs";
|
import { useDisplay, useTheme } from "vuetify/lib/framework.mjs";
|
||||||
import { ref } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import { navigateTo } from "#app";
|
import { navigateTo, useRouter } 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>
|
||||||
|
@ -13,22 +25,17 @@ const navOpen = ref(!mobile.value);
|
||||||
<VToolbarTitle>Database Project</VToolbarTitle>
|
<VToolbarTitle>Database Project</VToolbarTitle>
|
||||||
</VAppBar>
|
</VAppBar>
|
||||||
<VNavigationDrawer v-model="navOpen" :temporary="mobile">
|
<VNavigationDrawer v-model="navOpen" :temporary="mobile">
|
||||||
<!-- <VList>
|
<VList v-model:selected="navSelected" density="compact" nav>
|
||||||
<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>
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { order as orderType } from '~/utils/types/database';
|
import type { useFetch } from '#imports';
|
||||||
|
|
||||||
|
type Order = Awaited<ReturnType<typeof useFetch<void, any, "/api/orders/:id", "get">>>["data"]["value"];
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
order?: orderType
|
order?: Order | undefined
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -24,7 +26,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.is_fulfilled }}</td>
|
<td>{{ i.fulfilled }}</td>
|
||||||
<td>
|
<td>
|
||||||
{{ i.offer.name }}
|
{{ i.offer.name }}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
const count = ref(0);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<VBtn @click="count++">
|
|
||||||
Clicky {{ count }} times
|
|
||||||
</VBtn>
|
|
||||||
</template>
|
|
8
middleware/firstRun.ts
Normal file
8
middleware/firstRun.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
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" });
|
||||||
|
});
|
|
@ -1,10 +1,19 @@
|
||||||
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",
|
||||||
|
@ -18,7 +27,11 @@ export default defineNuxtConfig({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
vite: {
|
vite: {
|
||||||
// devBundler: "legacy"
|
vue: {
|
||||||
|
template: {
|
||||||
|
transformAssetUrls,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
imports: {
|
imports: {
|
||||||
autoImport: false,
|
autoImport: false,
|
||||||
|
|
9801
package-lock.json
generated
9801
package-lock.json
generated
File diff suppressed because it is too large
Load diff
13
package.json
13
package.json
|
@ -13,15 +13,18 @@
|
||||||
"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": "^5.59.0",
|
"@typescript-eslint/eslint-plugin": "^6.9.1",
|
||||||
"@typescript-eslint/parser": "^5.59.0",
|
"@typescript-eslint/parser": "^6.9.1",
|
||||||
"eslint": "^8.39.0",
|
"eslint": "^8.39.0",
|
||||||
"nuxt": "3.4.0",
|
"nuxt": "3.8.2",
|
||||||
|
"prisma": "5.7.1",
|
||||||
"sass": "^1.62.0",
|
"sass": "^1.62.0",
|
||||||
"vite-plugin-vuetify": "^1.0.2",
|
"vite-plugin-vuetify": "^2.0.1",
|
||||||
"vuetify": "^3.1.15"
|
"vuetify": "^3.1.15"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mysql2": "^3.2.3"
|
"@prisma/client": "5.7.1",
|
||||||
|
"@prisma/engines": "5.7.1",
|
||||||
|
"@prisma/migrate": "5.7.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,34 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/* global $fetch */
|
import { ref, type Ref } from "vue";
|
||||||
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, { fieldDefinition } from "~/components/entryEditor.vue";
|
import EntryEditor, { type 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}`);
|
const clientRequest = await useFetch(`/api/clients/${id}` as "/api/clients/:id");
|
||||||
if (clientRequest.error.value) throw createError(clientRequest.error.value?.data ?? "");
|
if (clientRequest.error.value) throw createError(clientRequest.error.value?.data ?? "");
|
||||||
const client = clientRequest.data as Ref<clientType>;
|
type Client = NonNullable<typeof clientRequest.data.value>;
|
||||||
|
const client = clientRequest.data as Ref<Client>;
|
||||||
|
|
||||||
const clientOrdersRequest = await useFetch(`/api/clients/${id}/orders`);
|
const clientOrdersRequest = await useFetch(`/api/clients/${id}/orders` as "/api/clients/:id/orders");
|
||||||
if (clientOrdersRequest.error.value) throw createError(clientOrdersRequest.error.value?.data ?? "");
|
if (clientOrdersRequest.error.value) throw createError(clientOrdersRequest.error.value?.data ?? "");
|
||||||
const clientOrders = clientOrdersRequest.data as Ref<Array<orderSummary>>;
|
type OrderSummary = NonNullable<typeof clientOrdersRequest.data.value>;
|
||||||
|
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)
|
||||||
|
@ -34,9 +39,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,10 +56,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 ?? undefined },
|
{ key: "name", type: "text", label: "Name", value: client.value.name ?? "" },
|
||||||
{ key: "address", type: "text", label: "Address", value: client.value.address ?? undefined },
|
{ key: "address", type: "text", label: "Address", value: client.value.address ?? "" },
|
||||||
{ key: "phone", type: "text", label: "Phone", value: client.value.phone ?? undefined },
|
{ key: "phone", type: "text", label: "Phone", value: client.value.phone ?? "" },
|
||||||
{ key: "email", type: "text", label: "E-mail", value: client.value.email ?? undefined },
|
{ key: "email", type: "text", label: "E-mail", value: client.value.email ?? "" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +78,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,
|
||||||
|
@ -184,24 +189,24 @@ function getCreationDate() {
|
||||||
>
|
>
|
||||||
<template #title="i">
|
<template #title="i">
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol>{{ new Date(Number(new Snowflake(BigInt(((i.record) as orderSummary).id)).timestamp)).toLocaleDateString() }}</VCol>
|
<VCol>{{ new Date(Number(new Snowflake(BigInt((i.record.id))).timestamp)).toLocaleDateString() }}</VCol>
|
||||||
<VCol>{{ ((i.record) as orderSummary).value }} PLN</VCol>
|
<VCol>{{ i.record.value }} PLN</VCol>
|
||||||
<VCol>
|
<VCol>
|
||||||
{{ ((i.record) as orderSummary).imported_products_count }}
|
{{ i.record.imported_products_count }}
|
||||||
products,
|
products,
|
||||||
{{ ((i.record) as orderSummary).work_count }}
|
{{ i.record.work_count }}
|
||||||
works
|
works
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</template>
|
</template>
|
||||||
<template #text="i">
|
<template #text="i">
|
||||||
<VProgressLinear
|
<VProgressLinear
|
||||||
:height="orders.get((i.record as orderSummary).id)?.loading ?? true ? undefined : 0"
|
:height="orders.get(i.record.id)?.loading ?? true ? undefined : 0"
|
||||||
absolute
|
absolute
|
||||||
:progress="orders.get((i.record as orderSummary).id)?.loading ?? true"
|
:progress="orders.get(i.record.id)?.loading ?? true"
|
||||||
:indeterminate="orders.get((i.record as orderSummary).id)?.loading ?? true"
|
:indeterminate="orders.get(i.record.id)?.loading ?? true"
|
||||||
/>
|
/>
|
||||||
<OrderView :order="(orders.get((i.record as orderSummary).id)?.value as order | undefined)" />
|
<OrderView :order="orders.get(i.record.id)?.value" />
|
||||||
</template>
|
</template>
|
||||||
</PagedList>
|
</PagedList>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
|
|
@ -1,124 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,17 +1,17 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/* global $fetch */
|
import { type NuxtError } from "nuxt/app";
|
||||||
import { useFetch, createError, navigateTo, NuxtError, useRoute } from "nuxt/app";
|
import { ref, type Ref, reactive } from "vue";
|
||||||
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, { AlertData } from "~/components/alerts.vue";
|
import Alerts, { type AlertData } from "~/components/alerts.vue";
|
||||||
import { fieldDefinition } from "~/components/entryEditor.vue";
|
import { type 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<clientType>(`/api/clients/${client}`, {
|
await fetch(`/api/clients/${client}` as "/api/clients/:id", {
|
||||||
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,19 +84,21 @@ 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] === "" ? null : formData.value[i];
|
formData.value[i] = formData.value[i] === "" || formData.value[i] === undefined ? 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;
|
||||||
|
|
61
pages/firstRun.vue
Normal file
61
pages/firstRun.vue
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<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>
|
|
@ -1,28 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,23 +1,9 @@
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import Test from '~/components/test.vue';
|
import { navigateTo } from '#imports';
|
||||||
|
|
||||||
|
navigateTo("/clients");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>Hi mom!</h1>
|
<VBtn @click="navigateTo('/clients')" />
|
||||||
<br>
|
|
||||||
<Test />
|
|
||||||
<Test />
|
|
||||||
<Test />
|
|
||||||
<Test />
|
|
||||||
<Test /><br>
|
|
||||||
<VCard
|
|
||||||
text="..."
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
color: blue;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,35 +1,39 @@
|
||||||
<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 { navigateTo, useCookie, useFetch, useRoute } from "nuxt/app";
|
import { type CookieRef } from "#app";
|
||||||
import { cookieSettings } from "~/utils/cookieSettings";
|
import { cookieSettings } from "~/utils/cookieSettings";
|
||||||
import { definePageMeta } from "~/.nuxt/imports";
|
import { definePageMeta, navigateTo, useCookie, useFetch, useRoute, useRequestFetch, useRequestEvent } from "#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 loggedIn = ref<boolean>(useCookie("token", cookieSettings).value != null);
|
const tokenCookie = useCookie("token", cookieSettings) as CookieRef<string | undefined>;
|
||||||
|
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);
|
||||||
loggedIn.value = true;
|
tokenCookie.value = result.token;
|
||||||
password.value = "";
|
password.value = "";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(typeof e);
|
console.log(typeof e);
|
||||||
|
@ -44,9 +48,10 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +61,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 = "";
|
||||||
|
@ -66,7 +71,7 @@ async function updateUserInfo() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUserInfo();
|
await updateUserInfo();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
194
pages/orders.vue
Normal file
194
pages/orders.vue
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
<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>
|
|
@ -1,50 +0,0 @@
|
||||||
<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>
|
|
|
@ -6,8 +6,6 @@ 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: {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
118
schema.prisma
Normal file
118
schema.prisma
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
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.
|
@ -1,10 +1,10 @@
|
||||||
/* global defineEventHandler getQuery, createError */
|
import { defineEventHandler } from "h3";
|
||||||
|
|
||||||
import BaaPagination from "~/server/utils/baaPagination";
|
import getPaginatedParameters from "../utils/baaPageParsing";
|
||||||
import { client } from "~/utils/types/database";
|
import { database } from "../utils/database";
|
||||||
|
import { prismaToWeb } from "~/server/utils/prismaToWeb";
|
||||||
export const baaWrapper = new BaaPagination<client, "id">("clients", "id");
|
|
||||||
|
|
||||||
export default defineEventHandler((e) => {
|
export default defineEventHandler((e) => {
|
||||||
return baaWrapper.RESTget(e);
|
const pageParameters = getPaginatedParameters(e, 50, 200);
|
||||||
|
return database.client.findPaginated(pageParameters, {}).then(prismaToWeb);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
/* global defineEventHandler, createError, readBody */
|
import { defineEventHandler, readBody, setResponseStatus } from "h3";
|
||||||
|
import { type Client } from "@prisma/client";
|
||||||
|
|
||||||
import { baaWrapper } from "./clients.get";
|
import getRequestingUser from "../utils/getRequestingUser";
|
||||||
import { client } from "~/utils/types/database";
|
import { database } from "../utils/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",
|
||||||
|
@ -10,10 +15,10 @@ const clientKeys: Array<string> = [
|
||||||
"email",
|
"email",
|
||||||
];
|
];
|
||||||
|
|
||||||
export function checkIsClient(
|
export function checkIsClient<Patch extends boolean = boolean>(
|
||||||
value: any,
|
value: any,
|
||||||
required = false,
|
patch: Patch,
|
||||||
): value is Partial<Omit<client, "id">> {
|
): value is Patch extends true ? Partial<Omit<Client, "id">> : Omit<Client, "id"> {
|
||||||
const errors = new Map<string, string>();
|
const errors = new Map<string, string>();
|
||||||
|
|
||||||
if (typeof value !== "object") {
|
if (typeof value !== "object") {
|
||||||
|
@ -23,12 +28,12 @@ export function checkIsClient(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(typeof value.name === "string" || value.name === null || (!required && 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.address === "string" || value.address === null || (!required && value.address === undefined))) errors.set("address", "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.phone === "string" || value.phone === null || (!required && value.phone === undefined))) errors.set("phone", "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.email === "string" || value.email === null || (!required && value.email === undefined))) errors.set("email", "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");
|
||||||
|
|
||||||
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) {
|
||||||
|
@ -48,6 +53,20 @@ export function checkIsClient(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler((e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
return baaWrapper.RESTpost(e, clientKeys as Array<keyof Omit<client, "id">>, (o): o is Omit<client, "id"> => checkIsClient(o, true));
|
const body = await readBody(e);
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,22 @@
|
||||||
/* global defineEventHandler */
|
import { defineEventHandler } from "h3";
|
||||||
|
|
||||||
import { baaWrapper } from "../clients.get";
|
import { database } from "~/server/utils/database";
|
||||||
|
|
||||||
export default defineEventHandler((e) => {
|
import { createError } from "#imports";
|
||||||
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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
/* global defineEventHandler */
|
import { defineEventHandler } from "h3";
|
||||||
|
|
||||||
import { baaWrapper } from "../clients.get";
|
import { database } from "~/server/utils/database";
|
||||||
|
import { prismaToWeb } from "~/server/utils/prismaToWeb";
|
||||||
|
|
||||||
export default defineEventHandler((e) => {
|
import { createError } from "#imports";
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,23 @@
|
||||||
/* global defineEventHandler */
|
import { defineEventHandler, readBody } from "h3";
|
||||||
|
|
||||||
import { checkIsClient } from "../clients.post";
|
import { checkIsClient } from "../clients.post";
|
||||||
import { baaWrapper } from "../clients.get";
|
import { database } from "~/server/utils/database";
|
||||||
|
import { prismaToWeb } from "~/server/utils/prismaToWeb";
|
||||||
|
|
||||||
export default defineEventHandler((e) => {
|
import { createError } from "#imports";
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
/* global defineEventHandler */
|
import { defineEventHandler } from "h3";
|
||||||
|
|
||||||
import { baaWrapper } from "~/server/api/orders.get";
|
import { getOrders } from "~/server/api/orders.get";
|
||||||
|
import { prismaToWeb } from "~/server/utils/prismaToWeb";
|
||||||
|
import getPaginatedParameters from "~/server/utils/baaPageParsing";
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler((e) => {
|
||||||
const baa = await baaWrapper.RESTget(e, 50, 200, "`client` = ?", [e.context.params?.id]);
|
const pageParameters = getPaginatedParameters(e, 50, 200);
|
||||||
console.log(baa);
|
const clientId = e.context.params?.id as string;
|
||||||
return baa;
|
return getOrders(
|
||||||
|
pageParameters,
|
||||||
|
{
|
||||||
|
clientId: BigInt(clientId),
|
||||||
|
},
|
||||||
|
).then(prismaToWeb);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
/* global defineEventHandler */
|
import { defineEventHandler } from "h3";
|
||||||
|
|
||||||
import { baaWrapper } from "../clients.get";
|
import { database } from "~/server/utils/database";
|
||||||
|
|
||||||
export default defineEventHandler((e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
return baaWrapper.RESTrecordCount(e);
|
return {
|
||||||
|
count: await database.client.count({}),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
/* 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}[];
|
|
||||||
});
|
|
|
@ -1,26 +0,0 @@
|
||||||
/* 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,
|
|
||||||
};
|
|
||||||
});
|
|
|
@ -1,9 +0,0 @@
|
||||||
/* 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]);
|
|
||||||
});
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global defineEventHandler */
|
import { defineEventHandler } from "h3";
|
||||||
|
|
||||||
export default defineEventHandler((event) => {
|
export default defineEventHandler((event) => {
|
||||||
const message = event.node.req.read();
|
const message = event.node.req.read();
|
||||||
|
|
16
server/api/firstRun.get.ts
Normal file
16
server/api/firstRun.get.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
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();
|
||||||
|
});
|
52
server/api/firstRun.post.ts
Normal file
52
server/api/firstRun.post.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
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;
|
||||||
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
/* global defineEventHandler */
|
import { defineEventHandler } from "h3";
|
||||||
|
|
||||||
export default defineEventHandler(() => {
|
export default defineEventHandler(() => {
|
||||||
return "Hi mom!";
|
return "Hi mom!";
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
/* global defineEventHandler, getCookie, setCookie, readBody, createError */
|
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
import { defineEventHandler, getCookie, setCookie, readBody } from "h3";
|
||||||
|
|
||||||
import { database, data } from "../utils/database";
|
import { database } from "../utils/database";
|
||||||
import { isString } from "../utils/isString";
|
import { isString } from "../utils/isString";
|
||||||
import { cookieSettings } from "../utils/rootUtils";
|
import { cookieSettings } from "../utils/rootUtils";
|
||||||
import Snowflake from "~/utils/snowflake";
|
import SessionToken from "../utils/SessionToken";
|
||||||
|
|
||||||
|
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"))
|
||||||
|
@ -18,23 +26,25 @@ 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 = crypto.createHmac("sha512", "42")
|
const hashedPassword = getPasswordHash(password);
|
||||||
.update(password)
|
|
||||||
.digest("hex");
|
|
||||||
|
|
||||||
const [account] = await database.query(
|
const account = await database.user.findUnique({
|
||||||
"SELECT CONVERT(`id`, CHAR(32)) AS `id` from `users` WHERE `username` = ? AND LOWER(HEX(`password`)) = ? LIMIT 1",
|
where: {
|
||||||
[login, hashedPassword],
|
username: login,
|
||||||
)as unknown as data<{id: string}>;
|
password: hashedPassword,
|
||||||
|
},
|
||||||
if (account.length === 0) throw createError({ statusCode: 400, message: "Invalid username or password." });
|
select: {
|
||||||
|
id: true,
|
||||||
const sessionId = new Snowflake().toString();
|
},
|
||||||
|
});
|
||||||
await database.query(
|
|
||||||
"INSERT INTO `sessions` (`id`, `user`) VALUES ( ? , ? )",
|
if (account === null) throw createError({ statusCode: 400, message: "Invalid username or password." });
|
||||||
[sessionId, account[0].id],
|
|
||||||
);
|
const session = new SessionToken(account.id);
|
||||||
setCookie(e, "token", sessionId, cookieSettings);
|
|
||||||
return { message: "Login successful", token: sessionId };
|
await database.session.create({
|
||||||
|
data: session.toPrisma(),
|
||||||
|
});
|
||||||
|
setCookie(e, "token", session.toString(), cookieSettings);
|
||||||
|
return { message: "Login successful", token: session.toString() };
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
/* global defineEventHandler, createError, getCookie, deleteCookie */
|
import { defineEventHandler, getCookie, deleteCookie } from "h3";
|
||||||
|
|
||||||
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) {
|
||||||
|
@ -21,9 +23,10 @@ export default defineEventHandler(async (e) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
database.query(
|
database.session.delete({
|
||||||
"DELETE FROM `sessions` WHERE `id` = ?",
|
where: {
|
||||||
[token],
|
id: BigInt(token),
|
||||||
);
|
},
|
||||||
|
});
|
||||||
return { message: "Logged out" };
|
return { message: "Logged out" };
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,37 +1,65 @@
|
||||||
/* global defineEventHandler */
|
import { defineEventHandler } from "h3";
|
||||||
|
import { type Order, type Client, Prisma } from "@prisma/client";
|
||||||
|
|
||||||
import BaaPagination from "../utils/baaPagination";
|
import getPaginatedParameters, { type pageData } from "../utils/baaPageParsing";
|
||||||
import { data, database } from "../utils/database";
|
import { database } from "../utils/database";
|
||||||
import { client, orderSummary } from "~/utils/types/database";
|
import { prismaToWeb } from "~/server/utils/prismaToWeb";
|
||||||
|
|
||||||
export const baaWrapper = new BaaPagination<orderSummary, "id">(
|
type orderSummary = Omit<Order, "clientId"> & {
|
||||||
"orderSummaries",
|
client: Client;
|
||||||
"id",
|
value: number;
|
||||||
"*, CONVERT(`client`, CHAR) AS `client`, CONVERT(`user`, CHAR) as `user`",
|
imported_products_count: number;
|
||||||
|
work_count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getOrders(
|
||||||
|
pageParameters: pageData,
|
||||||
|
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,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
const rvalue = new Array<orderSummary>();
|
||||||
const orders = await baaWrapper.RESTget(e, 50, 200);
|
|
||||||
|
|
||||||
const uniqueClients: Array<string> = [];
|
for (const i of data) {
|
||||||
for (const i of orders) {
|
const importedProductsPriceSum = i.imported_products.reduce((pv, cv) => pv + cv.price.toNumber(), 0);
|
||||||
if (!uniqueClients.includes(i.client))
|
const workPriceSum = i.work.reduce((pv, cv) => pv + cv.price.toNumber(), 0);
|
||||||
uniqueClients.push(database.escape(i.client));
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const [clients] = await database.query(
|
|
||||||
["SELECT",
|
|
||||||
"*,",
|
|
||||||
"CONVERT(`id`, CHAR) AS `id`",
|
|
||||||
"FROM `clients`",
|
|
||||||
"WHERE `id` IN",
|
|
||||||
`(${uniqueClients.join(', ')})`,
|
|
||||||
].join(" "),
|
|
||||||
) as data<client>;
|
|
||||||
|
|
||||||
const rvalue: Array<Omit<typeof orders, "client"> | { client?: client }> = [];
|
|
||||||
|
|
||||||
for (const i of orders)
|
|
||||||
rvalue.push({ ...i, client: clients.find(e => i.client === e.id) });
|
|
||||||
return rvalue;
|
return rvalue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler((e) => {
|
||||||
|
const pageParameters = getPaginatedParameters(e, 50, 200);
|
||||||
|
return getOrders(pageParameters, {}).then(prismaToWeb);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,37 +1,44 @@
|
||||||
/* global defineEventHandler, createError, readBody, setResponseStatus */
|
import { defineEventHandler, readBody, setResponseStatus } from "h3";
|
||||||
|
import * as Prisma from "@prisma/client";
|
||||||
|
|
||||||
import { createValidationError, handleRecursedValidationError } from "../utils/validation";
|
import { createValidationError, handleRecursedValidationError } from "../utils/validation";
|
||||||
import { database as db } from "../utils/database";
|
import { database } 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";
|
||||||
|
|
||||||
type importedProduct = {
|
import { createError } from "#imports";
|
||||||
|
|
||||||
|
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 = {
|
type work<inOrder extends boolean = boolean> = {
|
||||||
offer: string,
|
orderId: inOrder extends true ? never : string,
|
||||||
|
offerId: string,
|
||||||
price: number,
|
price: number,
|
||||||
notes: string | null,
|
notes: string | null,
|
||||||
is_fulfilled: boolean | 0 | 1,
|
fulfilled: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
type order = {
|
type order = {
|
||||||
client: string,
|
clientId: string,
|
||||||
// user: string,
|
// userId: string,
|
||||||
is_draft: boolean | 0 | 1,
|
draft: boolean,
|
||||||
imported_products: Array<importedProduct>,
|
imported_products: Array<importedProduct<true>>,
|
||||||
work: Array<work>,
|
work: Array<work<true>>,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function checkIsWork<Patch extends boolean = boolean>(
|
export function checkIsWork<Patch extends boolean = boolean, inOrder extends boolean = boolean>(
|
||||||
value: any,
|
value: any,
|
||||||
patch: Patch,
|
patch: Patch,
|
||||||
): value is Patch extends true ? Partial<work> : work {
|
needsOrderId: inOrder,
|
||||||
|
): 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") {
|
||||||
|
@ -41,19 +48,24 @@ export function checkIsWork<Patch extends boolean = boolean>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(typeof value.offer === "string" || (patch && value.offer === undefined))) errors.set("offer", "is not string");
|
if (!(typeof value.orderId === "string" || (patch && value.orderId === undefined) || !needsOrderId)) errors.set("orderId", "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" || value.is_fulfilled === 0 || value.is_fulfilled === 1 || (patch && value.is_fulfilled === undefined))) errors.set("is_fulfilled", "is not boolean");
|
if (!(typeof value.is_fulfilled === "boolean" || (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>(
|
export function checkIsImportedProduct<Patch extends boolean = boolean, inOrder extends boolean = boolean>(
|
||||||
value: any,
|
value: any,
|
||||||
patch: Patch,
|
patch: Patch,
|
||||||
): value is Patch extends true ? Partial<importedProduct> : importedProduct {
|
needsOrderId: inOrder,
|
||||||
|
): 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") {
|
||||||
|
@ -63,10 +75,14 @@ export function checkIsImportedProduct<Patch extends boolean = boolean>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 || (patch && value.price === undefined))) errors.set("price", "is not number");
|
if (!(typeof value.price === "number" || (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);
|
||||||
|
|
||||||
|
@ -76,7 +92,7 @@ export function checkIsImportedProduct<Patch extends boolean = boolean>(
|
||||||
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<Pick<order, "client" | "is_draft">> : order {
|
): value is Patch extends true ? Partial<Omit<order, "imported_products" | "work">> : order {
|
||||||
const errors = new Map<string, string>();
|
const errors = new Map<string, string>();
|
||||||
|
|
||||||
if (typeof value !== "object") {
|
if (typeof value !== "object") {
|
||||||
|
@ -86,19 +102,22 @@ export function checkIsOrder<Patch extends boolean = boolean>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(typeof value.client === "string" || (patch && value.client === undefined))) errors.set("client", "is not string");
|
if (!(typeof value.clientId === "string" || (patch && value.clientId === undefined))) errors.set("clientId", "is not string");
|
||||||
if (!(typeof value.is_draft === "boolean" || value.is_draft === 0 || value.is_draft === 1 || (patch && value.is_draft === undefined))) errors.set("is_draft", "is not boolean");
|
if (!(typeof value.draft === "boolean" || (patch && value.is_draft === undefined))) errors.set("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);
|
checkIsImportedProduct(importedProducts[i], patch, false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleRecursedValidationError(e, errors, `imported_products[${i}]`);
|
handleRecursedValidationError(e, errors, `imported_products[${i}]`);
|
||||||
}
|
}
|
||||||
|
@ -109,7 +128,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);
|
checkIsWork(work[i], patch, false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleRecursedValidationError(e, errors, `work[${i}]`);
|
handleRecursedValidationError(e, errors, `work[${i}]`);
|
||||||
}
|
}
|
||||||
|
@ -124,49 +143,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().toString();
|
const id = new Snowflake().state;
|
||||||
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 });
|
||||||
|
|
||||||
const database = await db.new();
|
await database.order.create({
|
||||||
await database.beginTransaction();
|
data: {
|
||||||
|
clientId: BigInt(body.clientId),
|
||||||
await database.query(
|
draft: body.draft,
|
||||||
["INSERT INTO",
|
imported_products: {
|
||||||
"`orders`",
|
createMany: {
|
||||||
"VALUES",
|
data: body.imported_products.reduce(
|
||||||
"(?, ?, ?, ?)",
|
(pV, cV) => {
|
||||||
].join(" "),
|
pV.push({
|
||||||
[id, body.client, user.id, body.is_draft],
|
...cV,
|
||||||
);
|
id: new Snowflake().state,
|
||||||
|
});
|
||||||
const promises: Array<Promise<any>> = [];
|
return pV;
|
||||||
for (const i of body.imported_products) {
|
},
|
||||||
promises.push(database.query(
|
[] as Array<Omit<Prisma.Prisma.ImportedProductCreateManyOrderInput, "orderId">>,
|
||||||
["INSERT INTO",
|
),
|
||||||
"`imported_products`",
|
},
|
||||||
"VALUES",
|
},
|
||||||
"(?, ?, ?, ?, ?, ?)",
|
work: {
|
||||||
].join(" "),
|
createMany: {
|
||||||
[new Snowflake().toString(), id, i.name, i.link, i.price_imported, i.price],
|
data: body.work.reduce(
|
||||||
));
|
(pV, cV) => {
|
||||||
}
|
pV.push({
|
||||||
|
...cV,
|
||||||
for (const i of body.work) {
|
id: new Snowflake().state,
|
||||||
promises.push(database.query(
|
offerId: BigInt(cV.offerId),
|
||||||
["INSERT INTO",
|
});
|
||||||
"`work`",
|
return pV;
|
||||||
"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);
|
return getOrder(id).then(prismaToWeb);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
/* global defineEventHandler, createError */
|
import { defineEventHandler } from "h3";
|
||||||
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;
|
const id = e.context.params?.id as string;
|
||||||
|
|
||||||
const [result] = await database.query(
|
try {
|
||||||
"DELETE FROM `orders` WHERE `id` = ?",
|
await database.order.delete({
|
||||||
[id],
|
where: {
|
||||||
) as unknown as [ResultSetHeader];
|
id: BigInt(id),
|
||||||
|
},
|
||||||
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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,108 +1,39 @@
|
||||||
/* global defineEventHandler, createError */
|
import { defineEventHandler } from "h3";
|
||||||
|
import { createError } from "#imports";
|
||||||
|
|
||||||
import { offer as offerType, order } from "~/utils/types/database";
|
import { database } from "~/server/utils/database";
|
||||||
import { database, data } from "~/server/utils/database";
|
import { prismaToWeb } from "~/server/utils/prismaToWeb";
|
||||||
|
|
||||||
export async function orderExists(id: string) {
|
export async function orderExists(id: bigint) {
|
||||||
const [[exists]] = await database.query(
|
const exists = await database.order.findUnique({
|
||||||
"SELECT EXISTS(*) AS `exists` FROM `orders` WHERE `id` = ?",
|
where: {
|
||||||
[id],
|
id,
|
||||||
) as data<{exists: 0 | 1}>;
|
},
|
||||||
|
});
|
||||||
return exists.exists === 1;
|
return exists !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getImportedProducts(id: string) {
|
export async function getOrder(id: bigint) {
|
||||||
const [importedProducts] = await database.query(
|
const order = await database.order.findUnique({
|
||||||
["SELECT",
|
where: {
|
||||||
"CONVERT(`id`, CHAR) AS `id`,",
|
id,
|
||||||
"`name`,",
|
},
|
||||||
"`link`,",
|
include: {
|
||||||
"`price`,",
|
imported_products: true,
|
||||||
"`price_imported`",
|
work: {
|
||||||
"FROM `imported_products`",
|
include: {
|
||||||
"WHERE `order` = ?",
|
offer: true,
|
||||||
].join(" "),
|
},
|
||||||
[id],
|
},
|
||||||
) as data<{
|
client: true,
|
||||||
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;
|
const key = e.context.params?.id as string;
|
||||||
return getOrder(key as string);
|
return getOrder(BigInt(key)).then(prismaToWeb);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,17 +1,27 @@
|
||||||
/* global defineEventHandler, readBody, createError */
|
import { defineEventHandler, readBody } from "h3";
|
||||||
|
|
||||||
import { checkIsOrder } from "../orders.post";
|
import { checkIsOrder } from "../orders.post";
|
||||||
import { database as db } from "~/server/utils/database";
|
import { getOrder } 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 body = await readBody(e);
|
const body = await readBody(e);
|
||||||
const id = e.context.params?.id;
|
const id = e.context.params?.id as string;
|
||||||
|
|
||||||
if (!checkIsOrder(e, true)) throw createError({ message: "Invalid body", statusCode: 400 });
|
if (!checkIsOrder(body, true)) throw createError({ message: "Invalid body", statusCode: 400 });
|
||||||
|
|
||||||
const database = await db.new();
|
await database.order.update({
|
||||||
await database.beginTransaction();
|
where: {
|
||||||
|
id: BigInt(id),
|
||||||
for (const [k, v] of Object.entries(body))
|
},
|
||||||
database.query(`UPDATE TABLE \`orders\` SET \`${k}\` = ? WHERE \`id\` = ?`, [v, id]);
|
data: {
|
||||||
|
clientId: body.clientId ? BigInt(body.clientId) : undefined,
|
||||||
|
draft: body.draft,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return getOrder(BigInt(id)).then(prismaToWeb);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,12 +1,27 @@
|
||||||
/* global defineEventHandler, createError */
|
import { defineEventHandler } from "h3";
|
||||||
|
|
||||||
import { orderExists, getImportedProducts } from "../[id].get";
|
import { orderExists } 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 id = e.context.params?.id as string;
|
const orderId = e.context.params?.id as string;
|
||||||
|
|
||||||
if (!orderExists(id)) throw createError({ statusCode: 404 });
|
if (!(await orderExists(BigInt(orderId)))) throw createError({ statusCode: 404 });
|
||||||
|
|
||||||
const importedProducts = await getImportedProducts(id);
|
return database.importedProduct.findMany({
|
||||||
return importedProducts;
|
where: {
|
||||||
|
orderId: BigInt(orderId),
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
link: true,
|
||||||
|
name: true,
|
||||||
|
orderId: true,
|
||||||
|
price: true,
|
||||||
|
price_imported: true,
|
||||||
|
},
|
||||||
|
}).then(prismaToWeb);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,27 +1,33 @@
|
||||||
/* global defineEventHandler, readBody, createError, setResponseStatus */
|
import { defineEventHandler, readBody, setResponseStatus } from "h3";
|
||||||
|
|
||||||
import { checkIsImportedProduct } from "../../orders.post";
|
import { checkIsImportedProduct } from "../../orders.post";
|
||||||
import { getImportedProducts, orderExists } from "../[id].get";
|
import { 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 idImportedProducts = new Snowflake().toString();
|
const idImportedProduct = new Snowflake().state;
|
||||||
|
|
||||||
if (!orderExists(idOrder)) throw createError({ statusCode: 404 });
|
if (!await orderExists(BigInt(idOrder))) throw createError({ statusCode: 404 });
|
||||||
if (!checkIsImportedProduct(body, false)) throw createError({ message: "Invalid body", statusCode: 400 });
|
if (!checkIsImportedProduct(body, false, false)) throw createError({ message: "Invalid body", statusCode: 400 });
|
||||||
|
|
||||||
await database.query(
|
const rvalue = await database.importedProduct.create({
|
||||||
["INSERT INTO",
|
data: {
|
||||||
"`imported_products`",
|
id: idImportedProduct,
|
||||||
"VALUES",
|
link: body.link,
|
||||||
"(?, ?, ?, ?, ?, ?)",
|
name: body.name,
|
||||||
].join(" "),
|
orderId: BigInt(idOrder),
|
||||||
[idImportedProducts, idOrder, body.name, body.link, body.price_imported, body.price],
|
price: body.price,
|
||||||
);
|
price_imported: body.price_imported,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
setResponseStatus(e, 201);
|
setResponseStatus(e, 201);
|
||||||
return getImportedProducts(idOrder);
|
|
||||||
|
return prismaToWeb(rvalue);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,12 +1,31 @@
|
||||||
/* global defineEventHandler, createError */
|
import { defineEventHandler } from "h3";
|
||||||
|
|
||||||
import { orderExists, getWork } from "../[id].get";
|
import { orderExists } 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 id = e.context.params?.id as string;
|
const orderId = e.context.params?.id as string;
|
||||||
|
|
||||||
if (!orderExists(id)) throw createError({ statusCode: 404 });
|
if (!await orderExists(BigInt(orderId))) throw createError({ statusCode: 404 });
|
||||||
|
|
||||||
const work = await getWork(id);
|
const data = await database.work.findMany({
|
||||||
return work;
|
where: {
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,28 +1,34 @@
|
||||||
/* global defineEventHandler, readBody, createError, setResponseStatus */
|
import { defineEventHandler, readBody, setResponseStatus } from "h3";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
import { checkIsWork } from "../../orders.post";
|
import { checkIsWork } from "../../orders.post";
|
||||||
import { getWork, orderExists } from "../[id].get";
|
import { 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().toString();
|
const idWork = new Snowflake().state;
|
||||||
|
|
||||||
if (!orderExists(idOrder)) throw createError({ statusCode: 404 });
|
if (!orderExists(BigInt(idOrder))) throw createError({ statusCode: 404 });
|
||||||
if (!checkIsWork(body, false)) throw createError({ message: "Invalid body", statusCode: 400 });
|
if (!checkIsWork(body, false, false)) throw createError({ message: "Invalid body", statusCode: 400 });
|
||||||
|
|
||||||
await database.query(
|
const rvalue = await database.work.create({
|
||||||
["INSERT INTO",
|
data: {
|
||||||
"`work`",
|
id: BigInt(idWork),
|
||||||
"VALUES",
|
fulfilled: body.fulfilled,
|
||||||
"(?, ?, ?, ?, ?, ?)",
|
notes: body.notes,
|
||||||
].join(" "),
|
offerId: BigInt(body.offerId),
|
||||||
[idWork, idOrder, body.offer, body.price, body.notes, body.is_fulfilled],
|
orderId: BigInt(body.orderId),
|
||||||
);
|
price: new Prisma.Decimal(body.price),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
setResponseStatus(e, 201);
|
setResponseStatus(e, 201);
|
||||||
|
|
||||||
return getWork(idWork);
|
return prismaToWeb(rvalue);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,20 +1,24 @@
|
||||||
/* global defineEventHandler, createError */
|
import { defineEventHandler } from "h3";
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
if (!orderExists(idOrder)) throw createError({ statusCode: 404 });
|
try {
|
||||||
|
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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,32 @@
|
||||||
/* global defineEventHandler, createError */
|
import { defineEventHandler } from "h3";
|
||||||
|
|
||||||
import { orderExists, getWork } from "../../[id].get";
|
import { orderExists } from "../../[id].get";
|
||||||
|
import { database } from "~/server/utils/database";
|
||||||
|
import { prismaToWeb } from "~/server/utils/prismaToWeb";
|
||||||
|
|
||||||
export default defineEventHandler((e) => {
|
import { createError } from "#imports";
|
||||||
|
|
||||||
|
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 (!orderExists(idOrder)) throw createError({ statusCode: 404 });
|
if (!await orderExists(BigInt(idOrder))) throw createError({ statusCode: 404 });
|
||||||
return getWork(idWork);
|
const data = await database.work.findUnique({
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
|
|
9
server/api/orders/count.get.ts
Normal file
9
server/api/orders/count.get.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { defineEventHandler } from "h3";
|
||||||
|
|
||||||
|
import { database } from "~/server/utils/database";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
return {
|
||||||
|
count: await database.order.count({}),
|
||||||
|
};
|
||||||
|
});
|
|
@ -1,14 +1,8 @@
|
||||||
/* global defineEventHandler, getCookie */
|
import { defineEventHandler } from "h3";
|
||||||
|
|
||||||
import { database, data } from "~/server/utils/database";
|
import getRequestingUser from "~/server/utils/getRequestingUser";
|
||||||
import { user } from "~/utils/types/database";
|
import { prismaToWeb } from "~/server/utils/prismaToWeb";
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler((e) => {
|
||||||
const token = getCookie(e, "token");
|
return getRequestingUser(e).then(prismaToWeb);
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
/* global defineEventHandler, createError, getCookie */
|
import { defineEventHandler, getCookie } from "h3";
|
||||||
|
import SessionToken from "../utils/SessionToken";
|
||||||
|
|
||||||
import { database, data } from "~/server/utils/database";
|
import { database } from "~/server/utils/database";
|
||||||
|
import getRequestingUser from "~/server/utils/getRequestingUser";
|
||||||
|
|
||||||
|
import { createError } from "#imports";
|
||||||
|
|
||||||
const endpointsWithoutAuth: string[] = [
|
const endpointsWithoutAuth: string[] = [
|
||||||
"/dbtest",
|
"/dbtest",
|
||||||
|
@ -8,6 +12,7 @@ const endpointsWithoutAuth: string[] = [
|
||||||
"/hi",
|
"/hi",
|
||||||
"/login",
|
"/login",
|
||||||
"/logout",
|
"/logout",
|
||||||
|
"/firstRun",
|
||||||
];
|
];
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
|
@ -31,10 +36,18 @@ 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;
|
||||||
const [[session]] = await database.query(
|
try {
|
||||||
"SELECT EXISTS(SELECT `id` FROM `sessions` WHERE `id` = ? AND `expiry_date` >= NOW()) as `logged_in`",
|
await database.session.findUniqueOrThrow({
|
||||||
[token],
|
where: {
|
||||||
) as unknown as data<{logged_in: number}>;
|
...SessionToken.fromString(token).toPrisma(),
|
||||||
|
expiry_date: {
|
||||||
|
gte: new Date(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return session.logged_in === 1;
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
49
server/utils/SessionToken.ts
Normal file
49
server/utils/SessionToken.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
108
server/utils/baaPageParsing.ts
Normal file
108
server/utils/baaPageParsing.ts
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,261 +0,0 @@
|
||||||
/* 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +1,102 @@
|
||||||
import mysql, { Connection } from "mysql2/promise";
|
import { PrismaClient, Prisma } from "@prisma/client";
|
||||||
|
import * as PrismaEngines from "@prisma/engines";
|
||||||
|
|
||||||
const connectionOptions: mysql.ConnectionOptions = {
|
import { type pageData } from "./baaPageParsing";
|
||||||
host: process.env.DB_HOST,
|
|
||||||
port: Number(process.env.DB_PORT),
|
|
||||||
user: process.env.DB_USER,
|
|
||||||
password: process.env.DB_PASSWORD,
|
|
||||||
database: process.env.DB_SCHEMA,
|
|
||||||
decimalNumbers: true,
|
|
||||||
supportBigNumbers: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const database =
|
type model = PrismaClient[Uncapitalize<Prisma.ModelName>];
|
||||||
await mysql.createConnection(connectionOptions) as Connection & { new: () => Promise<Connection> };
|
|
||||||
database.new = () => { return mysql.createConnection(connectionOptions); };
|
|
||||||
|
|
||||||
export type data<T> = [T[], mysql.FieldPacket[]];
|
PrismaEngines.ensureBinariesExist();
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -1,24 +1,34 @@
|
||||||
/* global getCookie, createError */
|
import { getCookie, H3Event } from "h3";
|
||||||
import { H3Event } from "h3";
|
|
||||||
|
|
||||||
import { database, data } from "./database";
|
import { database } from "./database";
|
||||||
import { user } from "~/utils/types/database";
|
import SessionToken from "./SessionToken";
|
||||||
|
|
||||||
|
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");
|
||||||
const [[user]] = await database.query(
|
if (!cookie) throw createError("User not found");
|
||||||
["SELECT",
|
const { user } = await database.session.findUnique({
|
||||||
"CONVERT(`users`.`id`, CHAR) as `id`,",
|
where: {
|
||||||
"`users`.`username`,",
|
...SessionToken.fromString(cookie).toPrisma(),
|
||||||
"`users`.`email`,",
|
expiry_date: {
|
||||||
"`users`.`display_name`",
|
gte: new Date(),
|
||||||
"FROM",
|
},
|
||||||
"`sessions`",
|
},
|
||||||
"LEFT JOIN `users` ON `sessions`.`user` = `users`.`id`",
|
select: {
|
||||||
"WHERE `sessions`.`id` = ?",
|
user: {
|
||||||
].join(" "),
|
select: {
|
||||||
[cookie],
|
display_name: true,
|
||||||
) as data<user>;
|
email: true,
|
||||||
|
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;
|
||||||
|
|
44
server/utils/prismaToWeb.ts
Normal file
44
server/utils/prismaToWeb.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
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;
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
/* global createError */
|
import { createError } from "#imports";
|
||||||
|
|
||||||
export function createValidationError(errors: Map<string, string>) {
|
export function createValidationError(errors: Map<string, string>) {
|
||||||
let message = "Invalid parameters: ";
|
let message = "Invalid parameters: ";
|
||||||
|
|
87
shell.nix
Normal file
87
shell.nix
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
{ 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"
|
||||||
|
'';
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { CookieSerializeOptions } from "cookie-es";
|
import { type CookieSerializeOptions } from "cookie-es";
|
||||||
|
|
||||||
export const cookieSettings: CookieSerializeOptions = {
|
export const cookieSettings: CookieSerializeOptions = {
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
|
|
|
@ -1,99 +0,0 @@
|
||||||
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}`;
|
|
Loading…
Reference in a new issue