diff --git a/backend/src/configs/scaler.config.ts b/backend/src/configs/scaler.config.ts index bd0614c..8c329ce 100644 --- a/backend/src/configs/scaler.config.ts +++ b/backend/src/configs/scaler.config.ts @@ -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 }, diff --git a/backend/src/datamart/datamart.controller.ts b/backend/src/datamart/datamart.controller.ts index e973705..c6e66ad 100644 --- a/backend/src/datamart/datamart.controller.ts +++ b/backend/src/datamart/datamart.controller.ts @@ -14,38 +14,83 @@ * 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, }); }; diff --git a/backend/src/datamart/datamart.routes.ts b/backend/src/datamart/datamart.routes.ts index 8d824be..338c5fc 100644 --- a/backend/src/datamart/datamart.routes.ts +++ b/backend/src/datamart/datamart.routes.ts @@ -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) => { diff --git a/backend/src/datamart/datamartAdd.route.ts b/backend/src/datamart/datamartAdd.route.ts index 5f1efc6..de47672 100644 --- a/backend/src/datamart/datamartAdd.route.ts +++ b/backend/src/datamart/datamartAdd.route.ts @@ -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); diff --git a/backend/src/datamart/datamartUpdate.route.ts b/backend/src/datamart/datamartUpdate.route.ts new file mode 100644 index 0000000..7e5d170 --- /dev/null +++ b/backend/src/datamart/datamartUpdate.route.ts @@ -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; diff --git a/backend/src/datamart/getDatamart.route.ts b/backend/src/datamart/getDatamart.route.ts index 774e123..722d7e1 100644 --- a/backend/src/datamart/getDatamart.route.ts +++ b/backend/src/datamart/getDatamart.route.ts @@ -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, ).toString(); - const dataRan = await runDatamartQuery({ name, criteria }); + const dataRan = await runDatamartQuery({ name, options }); return apiReturn(res, { success: dataRan.success, level: "info", diff --git a/backend/src/db/schema/datamart.schema.ts b/backend/src/db/schema/datamart.schema.ts index 3d724a0..f80789f 100644 --- a/backend/src/db/schema/datamart.schema.ts +++ b/backend/src/db/schema/datamart.schema.ts @@ -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); diff --git a/backend/src/scaler/datamartAdd.spec.ts b/backend/src/scaler/datamartAdd.spec.ts index 7b1cdc3..8514269 100644 --- a/backend/src/scaler/datamartAdd.spec.ts +++ b/backend/src/scaler/datamartAdd.spec.ts @@ -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", }, }, }, diff --git a/backend/src/scaler/datamartUpdate.spec.ts b/backend/src/scaler/datamartUpdate.spec.ts new file mode 100644 index 0000000..5a6dc8b --- /dev/null +++ b/backend/src/scaler/datamartUpdate.spec.ts @@ -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", + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/backend/src/scaler/getDatamart.spec.ts b/backend/src/scaler/getDatamart.spec.ts index 2180fc2..df072d3 100644 --- a/backend/src/scaler/getDatamart.spec.ts +++ b/backend/src/scaler/getDatamart.spec.ts @@ -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", diff --git a/backend/uploads/98e2aea9baadfbc009187d3bec17e26c b/backend/uploads/98e2aea9baadfbc009187d3bec17e26c new file mode 100644 index 0000000..3eb259d --- /dev/null +++ b/backend/uploads/98e2aea9baadfbc009187d3bec17e26c @@ -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) diff --git a/backend/uploads/b14c926b9e320ae8f818faf5fc3db5ad b/backend/uploads/b14c926b9e320ae8f818faf5fc3db5ad new file mode 100644 index 0000000..e23a697 Binary files /dev/null and b/backend/uploads/b14c926b9e320ae8f818faf5fc3db5ad differ diff --git a/backend/uploads/c1a1c2e6b89a269eb33d3498ff71b996 b/backend/uploads/c1a1c2e6b89a269eb33d3498ff71b996 new file mode 100644 index 0000000..3eb259d --- /dev/null +++ b/backend/uploads/c1a1c2e6b89a269eb33d3498ff71b996 @@ -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) diff --git a/migrations/0004_steady_timeslip.sql b/migrations/0004_steady_timeslip.sql new file mode 100644 index 0000000..c5062b5 --- /dev/null +++ b/migrations/0004_steady_timeslip.sql @@ -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"; \ No newline at end of file diff --git a/migrations/0005_plain_bill_hollister.sql b/migrations/0005_plain_bill_hollister.sql new file mode 100644 index 0000000..8703582 --- /dev/null +++ b/migrations/0005_plain_bill_hollister.sql @@ -0,0 +1 @@ +ALTER TABLE "datamart" ADD CONSTRAINT "datamart_name_unique" UNIQUE("name"); \ No newline at end of file diff --git a/migrations/meta/0004_snapshot.json b/migrations/meta/0004_snapshot.json new file mode 100644 index 0000000..cbaf3ac --- /dev/null +++ b/migrations/meta/0004_snapshot.json @@ -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": {} + } +} \ No newline at end of file diff --git a/migrations/meta/0005_snapshot.json b/migrations/meta/0005_snapshot.json new file mode 100644 index 0000000..a95ee90 --- /dev/null +++ b/migrations/meta/0005_snapshot.json @@ -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": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 5e7f1df..1ab58f5 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -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 } ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 94cac5f..8e40269 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "@dotenvx/dotenvx": "^1.51.2", - "zod": "^4.2.1" + "multer": "^2.0.2" }, "devDependencies": { "@biomejs/biome": "2.3.8", @@ -24,6 +24,7 @@ "@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", @@ -58,7 +59,8 @@ "tsx": "^4.21.0", "typescript": "^5.9.3", "vite-tsconfig-paths": "^6.0.3", - "vitest": "^4.0.16" + "vitest": "^4.0.16", + "zod": "^4.2.1" } }, "node_modules/@aws-crypto/sha256-browser": { @@ -6023,6 +6025,16 @@ "tedious": "*" } }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "24.10.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.3.tgz", @@ -6791,6 +6803,12 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -7358,7 +7376,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, "node_modules/bundle-name": { @@ -7377,6 +7394,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -7843,6 +7871,21 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -10683,7 +10726,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -12676,7 +12718,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -12805,6 +12846,79 @@ "node": ">=18" } }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -12993,7 +13107,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14185,7 +14298,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -14501,7 +14613,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -14905,11 +15016,18 @@ "dev": true, "license": "MIT" }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -15686,6 +15804,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -15813,7 +15937,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/uuid": { @@ -16263,7 +16386,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4" @@ -16342,6 +16464,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 2e5b7e6..0d6bd4f 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@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", @@ -77,7 +78,8 @@ "zod": "^4.2.1" }, "dependencies": { - "@dotenvx/dotenvx": "^1.51.2" + "@dotenvx/dotenvx": "^1.51.2", + "multer": "^2.0.2" }, "config": { "commitizen": {