From b777d87e5a5bd0f6bfa39cbaa71dc9776f133686 Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Mon, 5 Jan 2026 20:06:15 -0600 Subject: [PATCH] feat(datamart): get, add, update queries --- backend/src/configs/scaler.config.ts | 13 +- backend/src/datamart/datamart.controller.ts | 65 +- backend/src/datamart/datamart.routes.ts | 2 + backend/src/datamart/datamartAdd.route.ts | 79 +- backend/src/datamart/datamartUpdate.route.ts | 156 ++++ backend/src/datamart/getDatamart.route.ts | 4 +- backend/src/db/schema/datamart.schema.ts | 6 +- backend/src/scaler/datamartAdd.spec.ts | 57 +- backend/src/scaler/datamartUpdate.spec.ts | 81 ++ backend/src/scaler/getDatamart.spec.ts | 2 +- .../uploads/98e2aea9baadfbc009187d3bec17e26c | 38 + .../uploads/b14c926b9e320ae8f818faf5fc3db5ad | Bin 0 -> 23538 bytes .../uploads/c1a1c2e6b89a269eb33d3498ff71b996 | 38 + migrations/0004_steady_timeslip.sql | 7 + migrations/0005_plain_bill_hollister.sql | 1 + migrations/meta/0004_snapshot.json | 814 +++++++++++++++++ migrations/meta/0005_snapshot.json | 822 ++++++++++++++++++ migrations/meta/_journal.json | 14 + package-lock.json | 145 ++- package.json | 4 +- 20 files changed, 2282 insertions(+), 66 deletions(-) create mode 100644 backend/src/datamart/datamartUpdate.route.ts create mode 100644 backend/src/scaler/datamartUpdate.spec.ts create mode 100644 backend/uploads/98e2aea9baadfbc009187d3bec17e26c create mode 100644 backend/uploads/b14c926b9e320ae8f818faf5fc3db5ad create mode 100644 backend/uploads/c1a1c2e6b89a269eb33d3498ff71b996 create mode 100644 migrations/0004_steady_timeslip.sql create mode 100644 migrations/0005_plain_bill_hollister.sql create mode 100644 migrations/meta/0004_snapshot.json create mode 100644 migrations/meta/0005_snapshot.json 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 0000000000000000000000000000000000000000..e23a6975e3904057f66a66e0d7dc56db78b72eaf GIT binary patch literal 23538 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 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": {