7 Commits

30 changed files with 2590 additions and 593 deletions

View File

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

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
# ---> Node # ---> Node
testFiles
# Logs # Logs
logs logs
*.log *.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 WORKDIR /app
# Copy package files # Copy package files
COPY package*.json ./ COPY . .
# Install production dependencies only # Install production dependencies only
RUN npm ci 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 WORKDIR /app
ENV PORT=3000
ENV DB_USER=admin
ENV DB_PASSWORD=changeme
# 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 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 createApp = async () => {
const log = createLogger({ module: "system", subModule: "main start" }); const log = createLogger({ module: "system", subModule: "main start" });
const app = express(); const app = express();
let baseUrl = "/"; let baseUrl = "";
if (process.env.NODE_ENV?.trim() !== "production") { if (process.env.NODE_ENV?.trim() !== "production") {
app.use(morgan("tiny"));
baseUrl = "/lst"; 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.set("trust proxy", true);
app.all(`${baseUrl}api/auth/*splat`, toNodeHandler(auth)); app.all(`${baseUrl}api/auth/*splat`, toNodeHandler(auth));
app.use(express.json()); app.use(express.json());

View File

@@ -3,7 +3,7 @@ import createApp from "./app.js";
import { createLogger } from "./src/logger/logger.controller.js"; import { createLogger } from "./src/logger/logger.controller.js";
import { connectProdSql } from "./src/prodSql/prodSqlConnection.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 start = async () => {
const log = createLogger({ module: "system", subModule: "main start" }); const log = createLogger({ module: "system", subModule: "main start" });

View File

@@ -9,6 +9,7 @@ import { apiReference } from "@scalar/express-api-reference";
// const port = 3000; // const port = 3000;
import type { OpenAPIV3_1 } from "openapi-types"; import type { OpenAPIV3_1 } from "openapi-types";
import { datamartAddSpec } from "../scaler/datamartAdd.spec.js"; import { datamartAddSpec } from "../scaler/datamartAdd.spec.js";
import { datamartUpdateSpec } from "../scaler/datamartUpdate.spec.js";
import { getDatamartSpec } from "../scaler/getDatamart.spec.js"; import { getDatamartSpec } from "../scaler/getDatamart.spec.js";
import { prodLoginSpec } from "../scaler/login.spec.js"; import { prodLoginSpec } from "../scaler/login.spec.js";
import { prodRestartSpec } from "../scaler/prodSqlRestart.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) => { 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 = { const fullSpec = {
...openApiBase, ...openApiBase,
paths: { paths: {
@@ -91,8 +101,7 @@ export const setupApiDocsRoutes = (baseUrl: string, app: Express) => {
...prodRestartSpec, ...prodRestartSpec,
...prodLoginSpec, ...prodLoginSpec,
...prodRegisterSpec, ...prodRegisterSpec,
...getDatamartSpec, ...mergedDatamart,
...datamartAddSpec,
// Add more specs here as you build features // 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 * 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 { returnFunc } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
type Data = { type Data = {
name: string; name: string;
criteria: string; options: string;
}; };
export const runDatamartQuery = async (data: Data) => { export const runDatamartQuery = async (data: Data) => {
// search the query db for the query by name // search the query db for the query by name
const dummyquery = { const { data: queryInfo, error: qIe } = await tryCatch(
name: "something", db.select().from(datamart).where(eq(datamart.name, data.name)),
query: "select * from tableA where start=[start] and end=[end]", );
};
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 // 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 // split the criteria by "," then and then update the query
if (data.criteria) { if (data.options) {
const params = new URLSearchParams(data.criteria); 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); 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({ return returnFunc({
success: true, success: true,
level: "info", level: "info",
module: "datamart", module: "datamart",
subModule: "query", subModule: "query",
message: `Data for: ${data.name}`, message: `Data for: ${data.name}`,
data: [{ data: datamartQuery }], data: queryRun.data,
notify: false, notify: false,
}); });
}; };

View File

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

View File

@@ -1,27 +1,96 @@
import fs from "node:fs";
import { Router } from "express"; import { Router } from "express";
import multer from "multer";
import z from "zod"; 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 { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router(); const r = Router();
const upload = multer({ dest: "uploads/" });
const newQuery = z.object({ const newQuery = z.object({
name: z.string().min(5), name: z.string().min(5),
description: z.string().min(30), description: z.string().min(30),
query: z.string().min(10), query: z.string().min(10).optional(),
options: z options: z
.string() .string()
.describe("This should be a set of keys separated by a comma") .describe("This should be a set of keys separated by a comma")
.optional(), .optional(),
}); });
r.post("/", async (req, res) => { r.post("/", upload.single("queryFile"), async (req, res) => {
try { try {
const v = newQuery.parse(req.body); 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) { } catch (err) {
if (err instanceof z.ZodError) { if (err instanceof z.ZodError) {
const flattened = z.flattenError(err); 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 * 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 * 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. * 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. * 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 new queries to be added
* allow for queries to be updated by id * allow for queries to be updated by id
* table will be * table will be
@@ -19,4 +20,41 @@
* add_user * add_user
* upd_date * upd_date
* upd_user * 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) => { r.get("/:name", async (req, res) => {
const { name } = req.params; const { name } = req.params;
const criteria = new URLSearchParams( const options = new URLSearchParams(
req.query as Record<string, string>, req.query as Record<string, string>,
).toString(); ).toString();
const dataRan = await runDatamartQuery({ name, criteria }); const dataRan = await runDatamartQuery({ name, options });
return apiReturn(res, { return apiReturn(res, {
success: dataRan.success, success: dataRan.success,
level: "info", level: "info",

View File

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

View File

@@ -1,4 +1,5 @@
import build from "pino-abstract-transport"; import build from "pino-abstract-transport";
import { db } from "../db/db.controller.js"; import { db } from "../db/db.controller.js";
import { logs } from "../db/schema/logs.schema.js"; import { logs } from "../db/schema/logs.schema.js";
import { tryCatch } from "../utils/trycatch.utils.js"; import { tryCatch } from "../utils/trycatch.utils.js";
@@ -41,3 +42,29 @@ export default async function () {
console.error("Error inserting log into database:", err); 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"; import pino, { type Logger } from "pino";
export const logLevel = process.env.LOG_LEVEL || "info"; export const logLevel = process.env.LOG_LEVEL || "info";
const isDev = process.env.NODE_ENV !== "production";
const transport = pino.transport({ const transport = pino.transport({
targets: [ 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 }, redact: { paths: ["email", "password"], remove: true },
}, },
transport, 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 => { export const createLogger = (bindings: Record<string, unknown>): Logger => {

View File

@@ -1,37 +1,40 @@
import type { OpenAPIV3_1 } from "openapi-types"; import type { OpenAPIV3_1 } from "openapi-types";
export const datamartAddSpec: OpenAPIV3_1.PathsObject = { export const datamartAddSpec: OpenAPIV3_1.PathsObject = {
"/api/datamart/add": { "/api/datamart": {
post: { post: {
summary: "Creates the new query", summary: "New datamart query",
description: "Queries can only be created on the main server.", 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"], tags: ["Datamart"],
requestBody: { requestBody: {
required: true, required: true,
content: { content: {
"application/json": { "multipart/form-data": {
schema: { schema: {
type: "object", type: "object",
required: ["username", "password", "email"], required: ["name", "description", "queryFile"],
properties: { properties: {
username: {
type: "string",
example: "jdoe",
},
name: { name: {
type: "string", type: "string",
format: "string", example: "active_av",
example: "joe", description: "Unique name for the query",
}, },
email: { description: {
type: "string", type: "string",
format: "email", example: "Gets active audio/visual records",
example: "joe.doe@alpla.net", description: "Short explanation of what this query does",
}, },
password: { options: {
type: "string", type: "string",
format: "password", example: "foo,baz",
example: "superSecretPassword", 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: { responses: {
"200": { "200": {
description: "User info", description: "Query successfully created",
content: { content: {
"application/json": { "application/json": {
schema: { schema: {
type: "object", type: "object",
properties: { properties: {
success: { success: { type: "boolean", example: true },
type: "boolean",
format: "true",
example: true,
},
message: { message: {
type: "string", type: "string",
example: "User was created", example: "active_av was just added",
}, },
}, },
}, },
@@ -61,20 +60,16 @@ export const datamartAddSpec: OpenAPIV3_1.PathsObject = {
}, },
}, },
"400": { "400": {
description: "Invalid Data was sent over", description: "Validation or input error",
content: { content: {
"application/json": { "application/json": {
schema: { schema: {
type: "object", type: "object",
properties: { properties: {
success: { success: { type: "boolean", example: false },
type: "boolean",
format: "false",
example: false,
},
message: { message: {
type: "string", 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", name: "name",
in: "path", in: "path",
required: true, required: false,
description: "Name to look up", description: "Name to look up",
schema: { schema: {
type: "string", 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 container_name: lst_app
ports: ports:
#- "${VITE_PORT:-4200}:4200" #- "${VITE_PORT:-4200}:4200"
- "4000:4200" - "3600:3000"
environment: environment:
- NODE_ENV=development - NODE_ENV=production
# - DATABASE_HOST=host.docker.internal - LOG_LEVEL=info
# - DATABASE_PORT=${DATABASE_PORT} - DATABASE_HOST=host.docker.internal
# - DATABASE_USER=${DATABASE_USER} - DATABASE_PORT=5433
# - DATABASE_PASSWORD=${DATABASE_PASSWORD} - DATABASE_USER=${DATABASE_USER}
# - DATABASE_DB=${DATABASE_DB} - DATABASE_PASSWORD=${DATABASE_PASSWORD}
- DATABASE_DB=${DATABASE_DB}
- PROD_SERVER=${PROD_SERVER} - PROD_SERVER=${PROD_SERVER}
- PROD_PLANT_TOKEN=${PROD_PLANT_TOKEN} - PROD_PLANT_TOKEN=${PROD_PLANT_TOKEN}
- PROD_USER=${PROD_USER} - PROD_USER=${PROD_USER}
- PROD_PASSWORD=${PROD_PASSWORD} - PROD_PASSWORD=${PROD_PASSWORD}
# - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
# - BETTER_AUTH_URL=${BETTER_AUTH_URL} - BETTER_AUTH_URL=${URL}
restart: unless-stopped restart: unless-stopped
# for all host including prod servers, plc's, printers, or other de # for all host including prod servers, plc's, printers, or other de
extra_hosts: # extra_hosts:
- "${PROD_SERVER}:${PROD_IP}" # - "${PROD_SERVER}:${PROD_IP}"
networks: # networks:
- default # - default
- logisticsNetwork # - logisticsNetwork
- mlan1 # #- mlan1
networks: # networks:
logisticsNetwork: # logisticsNetwork:
driver: macvlan # driver: macvlan
driver_opts: # driver_opts:
parent: eth0 # parent: eth0
ipam: # ipam:
config: # config:
- subnet: ${LOGISTICS_NETWORK} # - subnet: ${LOGISTICS_NETWORK}
gateway: ${LOGISTICS_GATEWAY} # gateway: ${LOGISTICS_GATEWAY}
mlan1: # mlan1:
driver: macvlan # driver: macvlan
driver_opts: # driver_opts:
parent: eth0 # parent: eth0
ipam: # ipam:
config: # config:
- subnet: ${MLAN1_NETWORK} # - subnet: ${MLAN1_NETWORK}
gateway: ${MLAN1_GATEWAY} # 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, "when": 1767020576353,
"tag": "0003_orange_vision", "tag": "0003_orange_vision",
"breakpoints": true "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:app": "cd backend && tsx watch server.ts",
"dev:db:migrate": "npx drizzle-kit push", "dev:db:migrate": "npx drizzle-kit push",
"dev:db:generate": "tsc && npx drizzle-kit generate --config=drizzle.config.ts", "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": "npm run specCheck && npm run lint && npm run dev:db:generate && npm run dev:db:migrate && npm run build:app",
"build:app": "ncc build backend/app.ts -o dist -m -s", "build:app": "tsc",
"build:docker": "docker compose up --force-recreate --build -d",
"lint": "tsc && biome lint", "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", "commit": "cz",
"changeset": "changeset", "changeset": "changeset",
"version": "changeset version", "version": "changeset version",
@@ -32,13 +34,14 @@
"@changesets/cli": "^2.27.0", "@changesets/cli": "^2.27.0",
"@commitlint/cli": "^18.4.0", "@commitlint/cli": "^18.4.0",
"@commitlint/config-conventional": "^18.4.0", "@commitlint/config-conventional": "^18.4.0",
"@scalar/express-api-reference": "^0.8.28",
"@swc/core": "^1.15.7", "@swc/core": "^1.15.7",
"@swc/jest": "^0.2.39", "@swc/jest": "^0.2.39",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/morgan": "^1.9.10", "@types/morgan": "^1.9.10",
"@types/mssql": "^9.1.8", "@types/mssql": "^9.1.8",
"@types/multer": "^2.0.0",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/nodemailer": "^7.0.4", "@types/nodemailer": "^7.0.4",
"@types/nodemailer-express-handlebars": "^4.0.6", "@types/nodemailer-express-handlebars": "^4.0.6",
@@ -47,37 +50,48 @@
"@types/swagger-jsdoc": "^6.0.4", "@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8", "@types/swagger-ui-express": "^4.1.8",
"@vercel/ncc": "^0.38.4", "@vercel/ncc": "^0.38.4",
"axios": "^1.13.2",
"better-auth": "^1.4.9",
"commitizen": "^4.3.0", "commitizen": "^4.3.0",
"cors": "^2.8.5",
"cz-conventional-changelog": "^3.3.0", "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", "npm-check-updates": "^19.1.2",
"openapi-types": "^12.1.3", "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", "supertest": "^7.1.4",
"ts-jest": "^29.4.6", "ts-jest": "^29.4.6",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"tsx": "^4.21.0", "tsx": "^4.21.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite-tsconfig-paths": "^6.0.3", "vite-tsconfig-paths": "^6.0.3",
"vitest": "^4.0.16", "vitest": "^4.0.16"
"zod": "^4.2.1"
}, },
"dependencies": { "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": { "config": {
"commitizen": { "commitizen": {