7 Commits

30 changed files with 2590 additions and 593 deletions

View File

@@ -1,4 +1,7 @@
node_modules
.git
.env
dist
Dockerfile
docker-compose.yml
npm-debug.log

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
# ---> Node
testFiles
# Logs
logs
*.log

View File

@@ -1,23 +1,44 @@
FROM node:24.12-alpine
###########
# Stage 1 #
###########
# Build stage with all dependencies
FROM node:24.12-alpine as build
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY . .
# Install production dependencies only
RUN npm ci
RUN npm build:app
RUN npm run build
# Copy built app from builder stage
COPY --from=builder /app/dist ./dist
###########
# Stage 2 #
###########
# Small final image with only whats needed to run
FROM node:24.12-alpine AS production
# Environment variables with defaults
ENV PORT=3000
ENV DB_USER=admin
ENV DB_PASSWORD=changeme
WORKDIR /app
# Copy package files first to install runtime deps
COPY package*.json ./
# curl install
RUN apk add --no-cache curl
# Only install production dependencies
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
ENV RUNNING_IN_DOCKER=true
EXPOSE 3000
# start the app up
CMD ["npm", "run", "start:docker"]
CMD ["node", "dist/index.js"]
# Add health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:3000/lst/api/stats || exit 1

View File

@@ -1,42 +0,0 @@
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json ./
RUN ls -la /app
#RUN mkdir frontend
#RUN mkdir lstDocs
#RUN mkdir controller
#COPY frontend/package*.json ./frontend
#COPY lstDocs/package*.json ./lstDocs
#COPY controller/index.html ./controller
RUN npm install
#RUN npm run install:front
#RUN npm run install:docs
# Build the Next.js app
FROM node:24-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
#COPY --from=deps /app/frontend/node_modules ./frontend/node_modules
#COPY --from=deps /app/lstDocs/node_modules ./lstDocs/node_modules
#COPY --from=deps /app/controller/index.html ./controller/index.html
#COPY . ./
RUN npm run build:app
#RUN npm run build:front
#RUN npm run build:docs
# Final stage
FROM node:24-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
#COPY --from=builder /app/frontend/dist ./frontend/dist
#COPY --from=builder /app/lstDocs/build ./lstDocs/build
#COPY --from=deps /app/controller/index.html ./controller/index.html
ENV NODE_ENV=production
ENV RUNNING_IN_DOCKER=true
ENV PORT=3000
EXPOSE 3000
CMD ["node", "dist/index.js"]

View File

@@ -9,13 +9,19 @@ import { lstCors } from "./src/utils/cors.utils.js";
const createApp = async () => {
const log = createLogger({ module: "system", subModule: "main start" });
const app = express();
let baseUrl = "/";
let baseUrl = "";
if (process.env.NODE_ENV?.trim() !== "production") {
app.use(morgan("tiny"));
baseUrl = "/lst";
}
// if we are running un docker lets use this.
if (process.env.RUNNING_IN_DOCKER) {
baseUrl = "/lst";
}
// well leave this active so we can monitor it to validate
app.use(morgan("tiny"));
app.set("trust proxy", true);
app.all(`${baseUrl}api/auth/*splat`, toNodeHandler(auth));
app.use(express.json());

View File

@@ -3,7 +3,7 @@ import createApp from "./app.js";
import { createLogger } from "./src/logger/logger.controller.js";
import { connectProdSql } from "./src/prodSql/prodSqlConnection.controller.js";
const port = Number(process.env.PORT);
const port = Number(process.env.PORT) || 3000;
const start = async () => {
const log = createLogger({ module: "system", subModule: "main start" });

View File

@@ -9,6 +9,7 @@ import { apiReference } from "@scalar/express-api-reference";
// const port = 3000;
import type { OpenAPIV3_1 } from "openapi-types";
import { datamartAddSpec } from "../scaler/datamartAdd.spec.js";
import { datamartUpdateSpec } from "../scaler/datamartUpdate.spec.js";
import { getDatamartSpec } from "../scaler/getDatamart.spec.js";
import { prodLoginSpec } from "../scaler/login.spec.js";
import { prodRestartSpec } from "../scaler/prodSqlRestart.spec.js";
@@ -82,6 +83,15 @@ export const openApiBase: OpenAPIV3_1.Document = {
};
export const setupApiDocsRoutes = (baseUrl: string, app: Express) => {
const mergedDatamart = {
"/api/datamart": {
...(getDatamartSpec["/api/datamart"] ?? {}),
...(datamartAddSpec["/api/datamart"] ?? {}),
...(datamartUpdateSpec["/api/datamart"] ?? {}),
},
"/api/datamart/{name}": getDatamartSpec["/api/datamart/{name}"],
};
const fullSpec = {
...openApiBase,
paths: {
@@ -91,8 +101,7 @@ export const setupApiDocsRoutes = (baseUrl: string, app: Express) => {
...prodRestartSpec,
...prodLoginSpec,
...prodRegisterSpec,
...getDatamartSpec,
...datamartAddSpec,
...mergedDatamart,
// Add more specs here as you build features
},

View File

@@ -14,38 +14,84 @@
* when a criteria is password over we will handle it by counting how many were passed up to 3 then deal with each one respectively
*/
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { datamart } from "../db/schema/datamart.schema.js";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
type Data = {
name: string;
criteria: string;
options: string;
};
export const runDatamartQuery = async (data: Data) => {
// search the query db for the query by name
const dummyquery = {
name: "something",
query: "select * from tableA where start=[start] and end=[end]",
};
const { data: queryInfo, error: qIe } = await tryCatch(
db.select().from(datamart).where(eq(datamart.name, data.name)),
);
if (qIe) {
return returnFunc({
success: false,
level: "error",
module: "datamart",
subModule: "query",
message: `Error getting ${data.name} info`,
data: [qIe],
notify: false,
});
}
// create the query with no changed just to have it here
let datamartQuery = dummyquery.query;
let datamartQuery = queryInfo[0]?.query || "";
// split the criteria by "," then and then update the query
if (data.criteria) {
const params = new URLSearchParams(data.criteria);
if (data.options) {
const params = new URLSearchParams(data.options);
for (const [key, value] of params.entries()) {
for (const [rawKey, rawValue] of params.entries()) {
const key = rawKey.trim();
const value = rawValue.trim();
datamartQuery = datamartQuery.replaceAll(`[${key}]`, value);
}
}
const { data: queryRun, error } = await tryCatch(
prodQuery(datamartQuery, `Running datamart query: ${data.name}`),
);
if (error) {
return returnFunc({
success: false,
level: "error",
module: "datamart",
subModule: "query",
message: `Data for: ${data.name} encountered an error while trying to get it`,
data: [error],
notify: false,
});
}
if (!queryRun.success) {
return returnFunc({
success: false,
level: "error",
module: "datamart",
subModule: "query",
message: queryRun.message,
data: queryRun.data,
notify: false,
});
}
return returnFunc({
success: true,
level: "info",
module: "datamart",
subModule: "query",
message: `Data for: ${data.name}`,
data: [{ data: datamartQuery }],
data: queryRun.data,
notify: false,
});
};

View File

@@ -4,6 +4,7 @@ import { db } from "../db/db.controller.js";
import { datamart } from "../db/schema/datamart.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import addQuery from "./datamartAdd.route.js";
import updateQuery from "./datamartUpdate.route.js";
import runQuery from "./getDatamart.route.js";
export const setupDatamartRoutes = (baseUrl: string, app: Express) => {
@@ -11,6 +12,7 @@ export const setupDatamartRoutes = (baseUrl: string, app: Express) => {
app.use(`${baseUrl}/api/datamart`, runQuery);
app.use(`${baseUrl}/api/datamart`, addQuery);
app.use(`${baseUrl}/api/datamart`, updateQuery);
// just sending a get on datamart will return all the queries that we can call.
app.get(`${baseUrl}/api/datamart`, async (_, res) => {

View File

@@ -1,27 +1,96 @@
import fs from "node:fs";
import { Router } from "express";
import multer from "multer";
import z from "zod";
import type { NewDatamart } from "../db/schema/datamart.schema.js";
import { db } from "../db/db.controller.js";
import { datamart, type NewDatamart } from "../db/schema/datamart.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
const upload = multer({ dest: "uploads/" });
const newQuery = z.object({
name: z.string().min(5),
description: z.string().min(30),
query: z.string().min(10),
query: z.string().min(10).optional(),
options: z
.string()
.describe("This should be a set of keys separated by a comma")
.optional(),
});
r.post("/", async (req, res) => {
r.post("/", upload.single("queryFile"), async (req, res) => {
try {
const v = newQuery.parse(req.body);
const query: NewDatamart = { ...v };
const query: NewDatamart = {
...v,
name: v.name?.trim().replaceAll(" ", "_"),
};
console.log(query);
//console.log(query);
if (req.file) {
const sqlContents = fs.readFileSync(req.file.path, "utf8");
query.query = sqlContents;
// optional: delete temp file afterwards
fs.unlink(req.file.path, () => {});
}
// if we forget the file crash out
if (!query.query) {
// no query text anywhere
return apiReturn(res, {
success: true,
level: "info", //connect.success ? "info" : "error",
module: "routes",
subModule: "datamart",
message: `${query.name} missing sql file to parse`,
data: [],
status: 400, //connect.success ? 200 : 400,
});
}
// // if we didn't replace the test1 stuff crash out
// if (!query.query.includes("test1")) {
// return apiReturn(res, {
// success: true,
// level: "info", //connect.success ? "info" : "error",
// module: "routes",
// subModule: "datamart",
// message:
// "Query must include the 'test1' or everything switched to test1",
// data: [],
// status: 400, //connect.success ? 200 : 400,
// });
// }
const { data, error } = await tryCatch(db.insert(datamart).values(query));
if (error) {
return apiReturn(res, {
success: true,
level: "error", //connect.success ? "info" : "error",
module: "routes",
subModule: "datamart",
message: `${query.name} encountered an error while being added`,
data: [error.cause],
status: 200, //connect.success ? 200 : 400,
});
}
if (data) {
return apiReturn(res, {
success: true,
level: "info", //connect.success ? "info" : "error",
module: "routes",
subModule: "datamart",
message: `${query.name} was just added`,
data: [query],
status: 200, //connect.success ? 200 : 400,
});
}
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);

View File

@@ -2,9 +2,10 @@
* If we are running in client mode we want to periodically check the SERVER_NAME for new/updates queries
* this will be on a cronner job, we will check 2 times a day for new data, we will also have a route we can trigger to check this manually incase we have
* queries we make for one plant but will eventually go to all plants.
* in client mode we will not be able to add, update, or delete
* in client mode we will not be able to add, update, or delete, or push updates
*
* if we are running on server mode we will provide all queries.
* when pushing to another server we will allow all or just a single server by plant token.
* allow for new queries to be added
* allow for queries to be updated by id
* table will be
@@ -19,4 +20,41 @@
* add_user
* upd_date
* upd_user
*
* if we are running in localhost or dev or just someone running the server on there computer but using localhost we will allow to push to the main server the SERVER_NAME in the env should point to the main server
* that way when we check if we are in production we will know.
* the node env must also be set non production in order to push to the main server.
* we will also be able to do all the same as the server mode but the push here will just go to the main server.
*/
// doing the client stuff first
// ┌──────────────── (optional) second (0 - 59)
// │ ┌────────────── minute (0 - 59)
// │ │ ┌──────────── hour (0 - 23)
// │ │ │ ┌────────── day of month (1 - 31)
// │ │ │ │ ┌──────── month (1 - 12, JAN-DEC)
// │ │ │ │ │ ┌────── day of week (0 - 6, SUN-Mon)
// │ │ │ │ │ │ (0 to 6 are Sunday to Saturday; 7 is Sunday, the same as 0)
// │ │ │ │ │ │
// * * * * * *
if (process.env.NODE_ENV?.trim() === "production") {
// setup cronner
let cronTime = "* 5 * * * *";
if (process.env.QUERY_TIME_TYPE === "m") {
// will run this cron ever x
cronTime = `* ${process.env.QUERY_CHECK} * * * *`;
}
if (process.env.QUERY_TIME_TYPE === "h") {
// will run this cron ever x
cronTime = `* * ${process.env.QUERY_CHECK} * * * `;
}
if (process.env.QUERY_TIME_TYPE === "d") {
// will run this cron ever x
cronTime = `* * * * * ${process.env.QUERY_CHECK}`;
}
console.info(cronTime);
}

View File

@@ -0,0 +1,156 @@
import fs from "node:fs";
import { eq, sql } from "drizzle-orm";
import { Router } from "express";
import multer from "multer";
import z from "zod";
import { db } from "../db/db.controller.js";
import { datamart } from "../db/schema/datamart.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
const upload = multer({ dest: "uploads/" });
const newQuery = z.object({
name: z.string().min(5).optional(),
description: z.string().min(30).optional(),
query: z.string().min(10).optional(),
options: z
.string()
.describe("This should be a set of keys separated by a comma")
.optional(),
setActive: z.string().optional(),
active: z.boolean().optional(),
});
r.patch("/:id", upload.single("queryFile"), async (req, res) => {
const { id } = req.params;
try {
const v = newQuery.parse(req.body);
const query = {
...v,
};
//console.log(query);
if (req.file) {
const sqlContents = fs.readFileSync(req.file.path, "utf8");
query.query = sqlContents;
// optional: delete temp file afterwards
fs.unlink(req.file.path, () => {});
}
if (v.name) {
query.name = v.name.trim().replaceAll(" ", "_");
}
if (v.description) {
query.options = v.description;
}
if (v.options) {
query.options = v.options;
}
if (v.setActive) {
query.active = v.setActive === "true";
}
// if we forget the file crash out
// if (!query.query) {
// // no query text anywhere
// return apiReturn(res, {
// success: true,
// level: "info", //connect.success ? "info" : "error",
// module: "routes",
// subModule: "datamart",
// message: `${query.name} missing sql file to parse`,
// data: [],
// status: 400, //connect.success ? 200 : 400,
// });
// }
// // if we didn't replace the test1 stuff crash out
if (query.query && !query.query.includes("test1")) {
return apiReturn(res, {
success: true,
level: "error", //connect.success ? "info" : "error",
module: "routes",
subModule: "datamart",
message:
"All queries must point to test1 this way we can keep it dynamic.",
data: [],
status: 400, //connect.success ? 200 : 400,
});
}
const { data, error } = await tryCatch(
db
.update(datamart)
.set({
...query,
version: sql`${datamart.version} + 1`,
upd_date: sql`NOW()`,
upd_user: "lst_user",
})
.where(eq(datamart.id, id as string)),
);
if (error) {
return apiReturn(res, {
success: true,
level: "error", //connect.success ? "info" : "error",
module: "routes",
subModule: "datamart",
message: `${query.name} encountered an error while being updated`,
data: [error.cause],
status: 200, //connect.success ? 200 : 400,
});
}
if (data) {
return apiReturn(res, {
success: true,
level: "info", //connect.success ? "info" : "error",
module: "routes",
subModule: "datamart",
message: `${query.name} was just updated`,
data: [],
status: 200, //connect.success ? 200 : 400,
});
}
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
// return res.status(400).json({
// error: "Validation failed",
// details: flattened,
// });
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "routes",
subModule: "auth",
message: "Validation failed",
data: [flattened],
status: 400, //connect.success ? 200 : 400,
});
}
return apiReturn(res, {
success: false,
level: "error",
module: "routes",
subModule: "datamart",
message: "There was an error updating the query",
data: [err],
status: 200,
});
}
});
export default r;

View File

@@ -6,11 +6,11 @@ const r = Router();
r.get("/:name", async (req, res) => {
const { name } = req.params;
const criteria = new URLSearchParams(
const options = new URLSearchParams(
req.query as Record<string, string>,
).toString();
const dataRan = await runDatamartQuery({ name, criteria });
const dataRan = await runDatamartQuery({ name, options });
return apiReturn(res, {
success: dataRan.success,
level: "info",

View File

@@ -11,16 +11,16 @@ import type { z } from "zod";
export const datamart = pgTable("datamart", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name"),
name: text("name").unique(),
description: text("description").notNull(),
query: text("query"),
version: integer("version").default(1).notNull(),
active: boolean("active").default(true),
options: text("checked").default(""),
options: text("options").default(""),
add_date: timestamp("add_date").defaultNow(),
add_user: text("add_user").default("lst-system"),
upd_date: timestamp("upd_date").defaultNow(),
upd_user: text("upd_date").default("lst-system"),
upd_user: text("upd_user").default("lst-system"),
});
export const datamartSchema = createSelectSchema(datamart);

View File

@@ -1,4 +1,5 @@
import build from "pino-abstract-transport";
import { db } from "../db/db.controller.js";
import { logs } from "../db/schema/logs.schema.js";
import { tryCatch } from "../utils/trycatch.utils.js";
@@ -41,3 +42,29 @@ export default async function () {
console.error("Error inserting log into database:", err);
}
}
// export const dbStream = {
// write: async (logString: string) => {
// try {
// const obj = JSON.parse(logString);
// const levelName = pinoLogLevels[obj.level] || "unknown";
// const res = await tryCatch(
// db.insert(logs).values({
// level: levelName,
// module: obj?.module?.toLowerCase(),
// subModule: obj?.subModule?.toLowerCase(),
// hostname: obj?.hostname?.toLowerCase(),
// message: obj.msg,
// stack: obj?.stack,
// }),
// );
// if (res.error) {
// console.error("DB log error:", res.error);
// }
// } catch (err) {
// console.error("Error parsing/inserting log:", err);
// }
// },
// };

View File

@@ -1,7 +1,7 @@
import pino, { type Logger } from "pino";
export const logLevel = process.env.LOG_LEVEL || "info";
const isDev = process.env.NODE_ENV !== "production";
const transport = pino.transport({
targets: [
{
@@ -13,7 +13,7 @@ const transport = pino.transport({
},
},
{
target: "./db.transport.ts",
target: isDev ? "./db.transport.ts" : "./db.transport.js",
},
],
});
@@ -24,6 +24,24 @@ const rootLogger: Logger = pino(
redact: { paths: ["email", "password"], remove: true },
},
transport,
// pino.multistream([
// // Pretty print to console in dev
// ...(isDev
// ? [
// {
// stream: pino.transport({
// target: "pino-pretty",
// options: { colorize: true },
// }),
// },
// ]
// : []),
// // Always log to database
// {
// level: "info",
// stream: dbStream,
// },
// ]),
);
export const createLogger = (bindings: Record<string, unknown>): Logger => {

View File

@@ -1,37 +1,40 @@
import type { OpenAPIV3_1 } from "openapi-types";
export const datamartAddSpec: OpenAPIV3_1.PathsObject = {
"/api/datamart/add": {
"/api/datamart": {
post: {
summary: "Creates the new query",
description: "Queries can only be created on the main server.",
summary: "New datamart query",
description:
"Creates a new query entry in the datamart. Must be called on the main server. The SQL can be provided as a file upload.",
tags: ["Datamart"],
requestBody: {
required: true,
content: {
"application/json": {
"multipart/form-data": {
schema: {
type: "object",
required: ["username", "password", "email"],
required: ["name", "description", "queryFile"],
properties: {
username: {
type: "string",
example: "jdoe",
},
name: {
type: "string",
format: "string",
example: "joe",
example: "active_av",
description: "Unique name for the query",
},
email: {
description: {
type: "string",
format: "email",
example: "joe.doe@alpla.net",
example: "Gets active audio/visual records",
description: "Short explanation of what this query does",
},
password: {
options: {
type: "string",
format: "password",
example: "superSecretPassword",
example: "foo,baz",
description:
"Optional comma separated options string passed to the query",
},
queryFile: {
type: "string",
format: "binary",
description: "SQL file containing the query text",
},
},
},
@@ -40,20 +43,16 @@ export const datamartAddSpec: OpenAPIV3_1.PathsObject = {
},
responses: {
"200": {
description: "User info",
description: "Query successfully created",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: {
type: "boolean",
format: "true",
example: true,
},
success: { type: "boolean", example: true },
message: {
type: "string",
example: "User was created",
example: "active_av was just added",
},
},
},
@@ -61,20 +60,16 @@ export const datamartAddSpec: OpenAPIV3_1.PathsObject = {
},
},
"400": {
description: "Invalid Data was sent over",
description: "Validation or input error",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: {
type: "boolean",
format: "false",
example: false,
},
success: { type: "boolean", example: false },
message: {
type: "string",
format: "Invalid Data was sent over.",
example: "Validation failed",
},
},
},

View File

@@ -0,0 +1,81 @@
import type { OpenAPIV3_1 } from "openapi-types";
export const datamartUpdateSpec: OpenAPIV3_1.PathsObject = {
"/api/datamart": {
patch: {
summary: "Update datamart query",
description:
"Update query entry in the datamart. Must be called on the main server. The SQL must be provided as a file upload.",
tags: ["Datamart"],
requestBody: {
required: true,
content: {
"multipart/form-data": {
schema: {
type: "object",
properties: {
name: {
type: "string",
example: "active_av",
description: "Unique name for the query",
},
description: {
type: "string",
example: "Gets active articles",
description: "Short explanation of what this query does",
},
options: {
type: "string",
example: "foo,baz",
description:
"Optional comma separated options string passed to the query",
},
queryFile: {
type: "string",
format: "binary",
description: "SQL file containing the query text",
},
},
},
},
},
},
responses: {
"200": {
description: "Query successfully created",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean", example: true },
message: {
type: "string",
example: "active_av was just added",
},
},
},
},
},
},
"400": {
description: "Validation or input error",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean", example: false },
message: {
type: "string",
example: "Validation failed",
},
},
},
},
},
},
},
},
},
};

View File

@@ -12,7 +12,7 @@ export const getDatamartSpec: OpenAPIV3_1.PathsObject = {
{
name: "name",
in: "path",
required: true,
required: false,
description: "Name to look up",
schema: {
type: "string",

View File

@@ -0,0 +1,38 @@
select [IdProdPlanung] as lot,
[IdArtikelvarianten] as av,
[IdProdBereich],
[IdMaschine],
PlanVon as StartLot,
PlanBis as EndLot,
-- Calculate total production time in hours
-- convert(float, DATEDIFF(MINUTE, PlanVon, PlanBis) / 60.0) totalProductionTime,
round(PlanDauer,2) as TimeToCompleteLot,
-- total production per hour
round(PlanMengePaletten / plandauer,2) as palletsPerHour,
--what time will it be in 24hours
DATEADD(hour, 24, getdate()) as Next24hours,
--time remaining
CASE WHEN DATEADD(hour, 24, getdate()) <= PlanBis THEN DATEDIFF(MINUTE, getdate(), DATEADD(hour, 24, getdate())) / 60.0
ELSE DATEDIFF(MINUTE, getdate(), PlanBis) / 60.0
END as TimeRemaining,
-- total pallets for the lot
PlanMengePaletten as TotalPallets,
--production rate per 24hours
round(CASE WHEN DATEADD(hour, 24, getdate()) <= PlanBis THEN (DATEDIFF(MINUTE, getdate(), DATEADD(hour, 24, getdate())) / 60.0) * (PlanMengePaletten / plandauer)
ELSE (DATEDIFF(MINUTE, getdate(), PlanBis) / 60.0 ) * (PlanMengePaletten / plandauer)
END,2) as PalletsNext24Hours,
--production rate per 12hours
round(CASE WHEN DATEADD(hour, 12, getdate()) <= PlanBis THEN (DATEDIFF(MINUTE, getdate(), DATEADD(hour, 12, getdate())) / 60.0) * (PlanMengePaletten / plandauer)
ELSE (DATEDIFF(MINUTE, getdate(), PlanBis) / 60.0 ) * (PlanMengePaletten / plandauer)
END,2) as PalletsNext12Hours,
round(CASE WHEN DATEADD(hour, 8, getdate()) <= PlanBis THEN (DATEDIFF(MINUTE, getdate(), DATEADD(hour, 8, getdate())) / 60.0) * (PlanMengePaletten / plandauer)
ELSE (DATEDIFF(MINUTE, getdate(), PlanBis) / 60.0 ) * (PlanMengePaletten / plandauer)
END,2) as PalletsNext8Hours,
Bemerkung as Remarks
from [AlplaPROD_usiow2].[dbo].[T_ProdPlanung] (nolock)
where PlanBis between getdate()-1 and getdate()+7 --IdProdPlanung in (266882,264642,267813)

Binary file not shown.

View File

@@ -0,0 +1,38 @@
select [IdProdPlanung] as lot,
[IdArtikelvarianten] as av,
[IdProdBereich],
[IdMaschine],
PlanVon as StartLot,
PlanBis as EndLot,
-- Calculate total production time in hours
-- convert(float, DATEDIFF(MINUTE, PlanVon, PlanBis) / 60.0) totalProductionTime,
round(PlanDauer,2) as TimeToCompleteLot,
-- total production per hour
round(PlanMengePaletten / plandauer,2) as palletsPerHour,
--what time will it be in 24hours
DATEADD(hour, 24, getdate()) as Next24hours,
--time remaining
CASE WHEN DATEADD(hour, 24, getdate()) <= PlanBis THEN DATEDIFF(MINUTE, getdate(), DATEADD(hour, 24, getdate())) / 60.0
ELSE DATEDIFF(MINUTE, getdate(), PlanBis) / 60.0
END as TimeRemaining,
-- total pallets for the lot
PlanMengePaletten as TotalPallets,
--production rate per 24hours
round(CASE WHEN DATEADD(hour, 24, getdate()) <= PlanBis THEN (DATEDIFF(MINUTE, getdate(), DATEADD(hour, 24, getdate())) / 60.0) * (PlanMengePaletten / plandauer)
ELSE (DATEDIFF(MINUTE, getdate(), PlanBis) / 60.0 ) * (PlanMengePaletten / plandauer)
END,2) as PalletsNext24Hours,
--production rate per 12hours
round(CASE WHEN DATEADD(hour, 12, getdate()) <= PlanBis THEN (DATEDIFF(MINUTE, getdate(), DATEADD(hour, 12, getdate())) / 60.0) * (PlanMengePaletten / plandauer)
ELSE (DATEDIFF(MINUTE, getdate(), PlanBis) / 60.0 ) * (PlanMengePaletten / plandauer)
END,2) as PalletsNext12Hours,
round(CASE WHEN DATEADD(hour, 8, getdate()) <= PlanBis THEN (DATEDIFF(MINUTE, getdate(), DATEADD(hour, 8, getdate())) / 60.0) * (PlanMengePaletten / plandauer)
ELSE (DATEDIFF(MINUTE, getdate(), PlanBis) / 60.0 ) * (PlanMengePaletten / plandauer)
END,2) as PalletsNext8Hours,
Bemerkung as Remarks
from [AlplaPROD_usiow2].[dbo].[T_ProdPlanung] (nolock)
where PlanBis between getdate()-1 and getdate()+7 --IdProdPlanung in (266882,264642,267813)

View File

@@ -6,44 +6,45 @@ services:
container_name: lst_app
ports:
#- "${VITE_PORT:-4200}:4200"
- "4000:4200"
- "3600:3000"
environment:
- NODE_ENV=development
# - DATABASE_HOST=host.docker.internal
# - DATABASE_PORT=${DATABASE_PORT}
# - DATABASE_USER=${DATABASE_USER}
# - DATABASE_PASSWORD=${DATABASE_PASSWORD}
# - DATABASE_DB=${DATABASE_DB}
- NODE_ENV=production
- LOG_LEVEL=info
- DATABASE_HOST=host.docker.internal
- DATABASE_PORT=5433
- DATABASE_USER=${DATABASE_USER}
- DATABASE_PASSWORD=${DATABASE_PASSWORD}
- DATABASE_DB=${DATABASE_DB}
- PROD_SERVER=${PROD_SERVER}
- PROD_PLANT_TOKEN=${PROD_PLANT_TOKEN}
- PROD_USER=${PROD_USER}
- PROD_PASSWORD=${PROD_PASSWORD}
# - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
# - BETTER_AUTH_URL=${BETTER_AUTH_URL}
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
- BETTER_AUTH_URL=${URL}
restart: unless-stopped
# for all host including prod servers, plc's, printers, or other de
extra_hosts:
- "${PROD_SERVER}:${PROD_IP}"
# extra_hosts:
# - "${PROD_SERVER}:${PROD_IP}"
networks:
- default
- logisticsNetwork
- mlan1
networks:
logisticsNetwork:
driver: macvlan
driver_opts:
parent: eth0
ipam:
config:
- subnet: ${LOGISTICS_NETWORK}
gateway: ${LOGISTICS_GATEWAY}
# networks:
# - default
# - logisticsNetwork
# #- mlan1
# networks:
# logisticsNetwork:
# driver: macvlan
# driver_opts:
# parent: eth0
# ipam:
# config:
# - subnet: ${LOGISTICS_NETWORK}
# gateway: ${LOGISTICS_GATEWAY}
mlan1:
driver: macvlan
driver_opts:
parent: eth0
ipam:
config:
- subnet: ${MLAN1_NETWORK}
gateway: ${MLAN1_GATEWAY}
# mlan1:
# driver: macvlan
# driver_opts:
# parent: eth0
# ipam:
# config:
# - subnet: ${MLAN1_NETWORK}
# gateway: ${MLAN1_GATEWAY}

View File

@@ -0,0 +1,7 @@
ALTER TABLE "datamart" ALTER COLUMN "version" SET DEFAULT 1;--> statement-breakpoint
ALTER TABLE "datamart" ALTER COLUMN "upd_date" SET DATA TYPE timestamp;--> statement-breakpoint
ALTER TABLE "datamart" ALTER COLUMN "upd_date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "datamart" ADD COLUMN "options" text DEFAULT '';--> statement-breakpoint
ALTER TABLE "datamart" ADD COLUMN "upd_user" text DEFAULT 'lst-system';--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "last_login";--> statement-breakpoint
ALTER TABLE "datamart" DROP COLUMN "checked";

View File

@@ -0,0 +1 @@
ALTER TABLE "datamart" ADD CONSTRAINT "datamart_name_unique" UNIQUE("name");

View File

@@ -0,0 +1,814 @@
{
"id": "0ad7997f-e800-4d5c-97eb-df0cdd93b43c",
"prevId": "f82bd918-f5f0-4c05-ba25-8a0e97891f5f",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"account_userId_idx": {
"name": "account_userId_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.apikey": {
"name": "apikey",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"start": {
"name": "start",
"type": "text",
"primaryKey": false,
"notNull": false
},
"prefix": {
"name": "prefix",
"type": "text",
"primaryKey": false,
"notNull": false
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"refill_interval": {
"name": "refill_interval",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"refill_amount": {
"name": "refill_amount",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"last_refill_at": {
"name": "last_refill_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"enabled": {
"name": "enabled",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": true
},
"rate_limit_enabled": {
"name": "rate_limit_enabled",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": true
},
"rate_limit_time_window": {
"name": "rate_limit_time_window",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 86400000
},
"rate_limit_max": {
"name": "rate_limit_max",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 10
},
"request_count": {
"name": "request_count",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 0
},
"remaining": {
"name": "remaining",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"last_request": {
"name": "last_request",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"permissions": {
"name": "permissions",
"type": "text",
"primaryKey": false,
"notNull": false
},
"metadata": {
"name": "metadata",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"apikey_key_idx": {
"name": "apikey_key_idx",
"columns": [
{
"expression": "key",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"apikey_userId_idx": {
"name": "apikey_userId_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"apikey_user_id_user_id_fk": {
"name": "apikey_user_id_user_id_fk",
"tableFrom": "apikey",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.jwks": {
"name": "jwks",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"public_key": {
"name": "public_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"private_key": {
"name": "private_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"impersonated_by": {
"name": "impersonated_by",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"session_userId_idx": {
"name": "session_userId_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": false
},
"banned": {
"name": "banned",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"ban_reason": {
"name": "ban_reason",
"type": "text",
"primaryKey": false,
"notNull": false
},
"ban_expires": {
"name": "ban_expires",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": false
},
"display_username": {
"name": "display_username",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
},
"user_username_unique": {
"name": "user_username_unique",
"nullsNotDistinct": false,
"columns": [
"username"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"verification_identifier_idx": {
"name": "verification_identifier_idx",
"columns": [
{
"expression": "identifier",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.datamart": {
"name": "datamart",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true
},
"query": {
"name": "query",
"type": "text",
"primaryKey": false,
"notNull": false
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
},
"active": {
"name": "active",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": true
},
"options": {
"name": "options",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "''"
},
"add_date": {
"name": "add_date",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"add_user": {
"name": "add_user",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'lst-system'"
},
"upd_date": {
"name": "upd_date",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"upd_user": {
"name": "upd_user",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'lst-system'"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.logs": {
"name": "logs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"level": {
"name": "level",
"type": "text",
"primaryKey": false,
"notNull": false
},
"module": {
"name": "module",
"type": "text",
"primaryKey": false,
"notNull": true
},
"subModule": {
"name": "subModule",
"type": "text",
"primaryKey": false,
"notNull": false
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": true
},
"stack": {
"name": "stack",
"type": "jsonb",
"primaryKey": false,
"notNull": false,
"default": "'[]'::jsonb"
},
"checked": {
"name": "checked",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"hostname": {
"name": "hostname",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,822 @@
{
"id": "f009fda6-7462-444a-8f2e-06dc61168d33",
"prevId": "0ad7997f-e800-4d5c-97eb-df0cdd93b43c",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"account_userId_idx": {
"name": "account_userId_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.apikey": {
"name": "apikey",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"start": {
"name": "start",
"type": "text",
"primaryKey": false,
"notNull": false
},
"prefix": {
"name": "prefix",
"type": "text",
"primaryKey": false,
"notNull": false
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"refill_interval": {
"name": "refill_interval",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"refill_amount": {
"name": "refill_amount",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"last_refill_at": {
"name": "last_refill_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"enabled": {
"name": "enabled",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": true
},
"rate_limit_enabled": {
"name": "rate_limit_enabled",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": true
},
"rate_limit_time_window": {
"name": "rate_limit_time_window",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 86400000
},
"rate_limit_max": {
"name": "rate_limit_max",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 10
},
"request_count": {
"name": "request_count",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 0
},
"remaining": {
"name": "remaining",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"last_request": {
"name": "last_request",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"permissions": {
"name": "permissions",
"type": "text",
"primaryKey": false,
"notNull": false
},
"metadata": {
"name": "metadata",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"apikey_key_idx": {
"name": "apikey_key_idx",
"columns": [
{
"expression": "key",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"apikey_userId_idx": {
"name": "apikey_userId_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"apikey_user_id_user_id_fk": {
"name": "apikey_user_id_user_id_fk",
"tableFrom": "apikey",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.jwks": {
"name": "jwks",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"public_key": {
"name": "public_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"private_key": {
"name": "private_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"impersonated_by": {
"name": "impersonated_by",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"session_userId_idx": {
"name": "session_userId_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": false
},
"banned": {
"name": "banned",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"ban_reason": {
"name": "ban_reason",
"type": "text",
"primaryKey": false,
"notNull": false
},
"ban_expires": {
"name": "ban_expires",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": false
},
"display_username": {
"name": "display_username",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
},
"user_username_unique": {
"name": "user_username_unique",
"nullsNotDistinct": false,
"columns": [
"username"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"verification_identifier_idx": {
"name": "verification_identifier_idx",
"columns": [
{
"expression": "identifier",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.datamart": {
"name": "datamart",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true
},
"query": {
"name": "query",
"type": "text",
"primaryKey": false,
"notNull": false
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
},
"active": {
"name": "active",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": true
},
"options": {
"name": "options",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "''"
},
"add_date": {
"name": "add_date",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"add_user": {
"name": "add_user",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'lst-system'"
},
"upd_date": {
"name": "upd_date",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"upd_user": {
"name": "upd_user",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'lst-system'"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"datamart_name_unique": {
"name": "datamart_name_unique",
"nullsNotDistinct": false,
"columns": [
"name"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.logs": {
"name": "logs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"level": {
"name": "level",
"type": "text",
"primaryKey": false,
"notNull": false
},
"module": {
"name": "module",
"type": "text",
"primaryKey": false,
"notNull": true
},
"subModule": {
"name": "subModule",
"type": "text",
"primaryKey": false,
"notNull": false
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": true
},
"stack": {
"name": "stack",
"type": "jsonb",
"primaryKey": false,
"notNull": false,
"default": "'[]'::jsonb"
},
"checked": {
"name": "checked",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"hostname": {
"name": "hostname",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -29,6 +29,20 @@
"when": 1767020576353,
"tag": "0003_orange_vision",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1767649080366,
"tag": "0004_steady_timeslip",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1767658930629,
"tag": "0005_plain_bill_hollister",
"breakpoints": true
}
]
}

666
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,10 +9,12 @@
"dev:app": "cd backend && tsx watch server.ts",
"dev:db:migrate": "npx drizzle-kit push",
"dev:db:generate": "tsc && npx drizzle-kit generate --config=drizzle.config.ts",
"build": "npm run specCheck && npm run lint && npm run dev:db:generate && npm run dev:db:migrate && npm run build:app && rimraf dist/backend",
"build:app": "ncc build backend/app.ts -o dist -m -s",
"build": "npm run specCheck && npm run lint && npm run dev:db:generate && npm run dev:db:migrate && npm run build:app",
"build:app": "tsc",
"build:docker": "docker compose up --force-recreate --build -d",
"lint": "tsc && biome lint",
"start": "dotenvx run -f .env -- node dist/index.js",
"start": "dotenvx run -f .env -- node dist/backend/server.js",
"start:docker": "node dist/backend/server.js",
"commit": "cz",
"changeset": "changeset",
"version": "changeset version",
@@ -32,13 +34,14 @@
"@changesets/cli": "^2.27.0",
"@commitlint/cli": "^18.4.0",
"@commitlint/config-conventional": "^18.4.0",
"@scalar/express-api-reference": "^0.8.28",
"@swc/core": "^1.15.7",
"@swc/jest": "^0.2.39",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/morgan": "^1.9.10",
"@types/mssql": "^9.1.8",
"@types/multer": "^2.0.0",
"@types/node": "^24.10.1",
"@types/nodemailer": "^7.0.4",
"@types/nodemailer-express-handlebars": "^4.0.6",
@@ -47,37 +50,48 @@
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8",
"@vercel/ncc": "^0.38.4",
"axios": "^1.13.2",
"better-auth": "^1.4.9",
"commitizen": "^4.3.0",
"cors": "^2.8.5",
"cz-conventional-changelog": "^3.3.0",
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.8.3",
"express": "^5.2.1",
"husky": "^8.0.3",
"morgan": "^1.10.1",
"mssql": "^12.2.0",
"nodemailer": "^7.0.12",
"nodemailer-express-handlebars": "^7.0.0",
"npm-check-updates": "^19.1.2",
"openapi-types": "^12.1.3",
"pg": "^8.16.3",
"pino": "^10.1.0",
"pino-pretty": "^13.1.3",
"postgres": "^3.4.7",
"supertest": "^7.1.4",
"ts-jest": "^29.4.6",
"ts-node-dev": "^2.0.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^6.0.3",
"vitest": "^4.0.16",
"zod": "^4.2.1"
"vitest": "^4.0.16"
},
"dependencies": {
"@dotenvx/dotenvx": "^1.51.2"
"@dotenvx/dotenvx": "^1.51.2",
"pino": "^10.1.0",
"pino-pretty": "^13.1.3",
"zod": "^4.2.1",
"pg": "^8.16.3",
"powershell": "^2.3.3",
"axios": "^1.13.2",
"better-auth": "^1.4.9",
"morgan": "^1.10.1",
"mssql": "^12.2.0",
"multer": "^2.0.2",
"nodemailer": "^7.0.12",
"nodemailer-express-handlebars": "^7.0.0",
"postgres": "^3.4.7",
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1",
"cors": "^2.8.5",
"croner": "^9.1.0",
"@scalar/express-api-reference": "^0.8.28",
"drizzle-zod": "^0.8.3",
"express": "^5.2.1",
"husky": "^8.0.3"
},
"config": {
"commitizen": {