From f60f600263c46661bb53548570bf5371e6699529 Mon Sep 17 00:00:00 2001 From: Hans Askov Date: Mon, 25 Nov 2024 23:15:15 +0100 Subject: [PATCH 001/341] Working example of backend in packages --- compose.override.yaml | 29 + compose.yaml | 20 +- packages/backend/.gitignore | 6 + packages/backend/README.md | 6 + packages/backend/biome.json | 30 + packages/backend/develop.dockerfile | 25 + packages/backend/dockerfile | 45 ++ packages/backend/drizzle.config.ts | 14 + .../backend/drizzle/0000_legal_the_fury.sql | 175 +++++ .../drizzle/0001_readings_hypertable.sql | 5 + .../backend/drizzle/0002_long_invaders.sql | 10 + .../0003_flippant_mikhail_rasputin.sql | 4 + packages/backend/drizzle/0004_far_lizard.sql | 4 + .../backend/drizzle/meta/0000_snapshot.json | 652 +++++++++++++++++ .../backend/drizzle/meta/0001_snapshot.json | 652 +++++++++++++++++ .../backend/drizzle/meta/0002_snapshot.json | 652 +++++++++++++++++ .../backend/drizzle/meta/0003_snapshot.json | 668 ++++++++++++++++++ .../backend/drizzle/meta/0004_snapshot.json | 668 ++++++++++++++++++ packages/backend/drizzle/meta/_journal.json | 41 ++ packages/backend/src/index.ts | 22 + packages/backend/tsconfig.json | 19 + packages/frontend/.dockerignore | 8 + packages/frontend/.gitignore | 21 + packages/frontend/.npmrc | 1 + packages/frontend/.prettierignore | 4 + packages/frontend/.prettierrc | 15 + packages/frontend/Caddyfile | 11 + packages/frontend/README.md | 21 + packages/frontend/develop.dockerfile | 26 + packages/frontend/dockerfile | 38 + packages/frontend/eslint.config.js | 33 + packages/frontend/src/app.d.ts | 13 + packages/frontend/src/app.html | 12 + packages/frontend/src/lib/index.ts | 1 + packages/frontend/src/routes/+layout.ts | 2 + packages/frontend/src/routes/+page.svelte | 34 + packages/frontend/static/favicon.png | Bin 0 -> 1571 bytes packages/frontend/svelte.config.js | 20 + packages/frontend/tsconfig.json | 19 + packages/frontend/vite.config.ts | 11 + 40 files changed, 4024 insertions(+), 13 deletions(-) create mode 100644 compose.override.yaml create mode 100644 packages/backend/.gitignore create mode 100644 packages/backend/README.md create mode 100644 packages/backend/biome.json create mode 100644 packages/backend/develop.dockerfile create mode 100644 packages/backend/dockerfile create mode 100644 packages/backend/drizzle.config.ts create mode 100644 packages/backend/drizzle/0000_legal_the_fury.sql create mode 100644 packages/backend/drizzle/0001_readings_hypertable.sql create mode 100644 packages/backend/drizzle/0002_long_invaders.sql create mode 100644 packages/backend/drizzle/0003_flippant_mikhail_rasputin.sql create mode 100644 packages/backend/drizzle/0004_far_lizard.sql create mode 100644 packages/backend/drizzle/meta/0000_snapshot.json create mode 100644 packages/backend/drizzle/meta/0001_snapshot.json create mode 100644 packages/backend/drizzle/meta/0002_snapshot.json create mode 100644 packages/backend/drizzle/meta/0003_snapshot.json create mode 100644 packages/backend/drizzle/meta/0004_snapshot.json create mode 100644 packages/backend/drizzle/meta/_journal.json create mode 100644 packages/backend/src/index.ts create mode 100644 packages/backend/tsconfig.json create mode 100644 packages/frontend/.dockerignore create mode 100644 packages/frontend/.gitignore create mode 100644 packages/frontend/.npmrc create mode 100644 packages/frontend/.prettierignore create mode 100644 packages/frontend/.prettierrc create mode 100644 packages/frontend/Caddyfile create mode 100644 packages/frontend/README.md create mode 100644 packages/frontend/develop.dockerfile create mode 100644 packages/frontend/dockerfile create mode 100644 packages/frontend/eslint.config.js create mode 100644 packages/frontend/src/app.d.ts create mode 100644 packages/frontend/src/app.html create mode 100644 packages/frontend/src/lib/index.ts create mode 100644 packages/frontend/src/routes/+layout.ts create mode 100644 packages/frontend/src/routes/+page.svelte create mode 100644 packages/frontend/static/favicon.png create mode 100644 packages/frontend/svelte.config.js create mode 100644 packages/frontend/tsconfig.json create mode 100644 packages/frontend/vite.config.ts diff --git a/compose.override.yaml b/compose.override.yaml new file mode 100644 index 00000000..1df85a13 --- /dev/null +++ b/compose.override.yaml @@ -0,0 +1,29 @@ +## Additional compose settings for development +## For more info see https://docs.docker.com/compose/how-tos/multiple-compose-files/merge/ + +services: + + caddy: + image: lucaslorentz/caddy-docker-proxy:ci-alpine + ports: + - "3000:3000" + + backend: + build: + context: . + dockerfile: ./packages/backend/develop.dockerfile + develop: + watch: + - action: sync + path: ./packages/backend + target: /app/packages/backend + + frontend: + build: + context: . + dockerfile: ./packages/frontend/develop.dockerfile + develop: + watch: + - action: sync + path: ./packages/frontend + target: /app/packages/frontend diff --git a/compose.yaml b/compose.yaml index 493634e3..dfa2135c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -18,16 +18,20 @@ services: ## The frontend service will build the frontend and then serve the files using caddy. frontend: profiles: [stateless] - build: services/frontend + build: + context: . + dockerfile: ./packages/frontend/dockerfile labels: caddy: "${INTERFACE_FQDN}" caddy.encode: zstd gzip - caddy.reverse_proxy: "{{upstreams 8080}}" + caddy.reverse_proxy: "{{upstreams 5173}}" ## The Backend will host our REST JSON api. Our spa will use this for data communication. backend: profiles: [stateless] - build: services/backend + build: + context: . + dockerfile: ./packages/backend/dockerfile environment: - DATABASE_URL=${DATABASE_URL} - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID} @@ -63,16 +67,6 @@ services: start_period: 30s timeout: 10s - ## Production insights - dozzle: - profiles: [prod] - image: amir20/dozzle:latest - volumes: - - /var/run/docker.sock:/var/run/docker.sock - labels: - caddy: "${DOZZLE_FQDN}" - caddy.reverse_proxy: "{{upstreams 8080}}" - volumes: caddy-data: caddy-config: diff --git a/packages/backend/.gitignore b/packages/backend/.gitignore new file mode 100644 index 00000000..e41acbb1 --- /dev/null +++ b/packages/backend/.gitignore @@ -0,0 +1,6 @@ +dist +node_modules +.env + +backend +backend.exe \ No newline at end of file diff --git a/packages/backend/README.md b/packages/backend/README.md new file mode 100644 index 00000000..36684cca --- /dev/null +++ b/packages/backend/README.md @@ -0,0 +1,6 @@ +# Backend + +This project is made to work in a monorepo with elysia and sveltekit. + +For development purposes please see the [README.MD](../../README.MD) file. + diff --git a/packages/backend/biome.json b/packages/backend/biome.json new file mode 100644 index 00000000..2eb07517 --- /dev/null +++ b/packages/backend/biome.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/packages/backend/develop.dockerfile b/packages/backend/develop.dockerfile new file mode 100644 index 00000000..5d111687 --- /dev/null +++ b/packages/backend/develop.dockerfile @@ -0,0 +1,25 @@ +# Use Bun (JavaScript runtime) image as the base +FROM oven/bun:1 AS build + +# Set the working directory to /app inside the container +WORKDIR /app + +# Copy necessary package files to install dependencies +COPY bun.lockb package.json /app/ +COPY /packages/backend/package.json /app/packages/backend/ +COPY /packages/frontend/package.json /app/packages/frontend/ + +# Install dependencies using Bun +RUN bun install + +# Copy the backend source code into the container +COPY /packages/backend /app/packages/backend + +# Move directory to frontend +WORKDIR /app/packages/backend + +# Start the backend server in watch mode for development +CMD ["bun", "--watch", "./src/index.ts"] + +# Expose port 3000 for the development server +EXPOSE 3000 \ No newline at end of file diff --git a/packages/backend/dockerfile b/packages/backend/dockerfile new file mode 100644 index 00000000..c7cbc2b2 --- /dev/null +++ b/packages/backend/dockerfile @@ -0,0 +1,45 @@ +# Use Bun (JavaScript runtime) image as the base +FROM oven/bun:1 AS build + +# Set the working directory to /app inside the container +WORKDIR /app + +# Copy necessary package files to install dependencies +COPY bun.lockb package.json /app/ +COPY /packages/backend/package.json /app/packages/backend/ +COPY /packages/frontend/package.json /app/packages/frontend/ + +# Install dependencies using Bun +RUN bun install + +# Copy the backend source code into the container +COPY /packages/backend /app/packages/backend + +# Build the backend using Bun +ENV NODE_ENV=production + +# Will create a binary executable with the name of server +RUN bun build \ + --compile \ + --minify-whitespace \ + --minify-syntax \ + --target bun \ + --outfile server \ + ./packages/backend/src/index.ts + +# Use a distroless image as the base +FROM gcr.io/distroless/base + +# Set the working directory to /app inside the container +WORKDIR /app + +# Copy the backend binary from the build stage +COPY --from=build /app/server server + +# Set the environment variable to production +ENV NODE_ENV=production + +# Run the backend server +CMD ["./server"] + +EXPOSE 3000 \ No newline at end of file diff --git a/packages/backend/drizzle.config.ts b/packages/backend/drizzle.config.ts new file mode 100644 index 00000000..23114bc6 --- /dev/null +++ b/packages/backend/drizzle.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'drizzle-kit'; + +if(!process.env.DATABASE_URL) { + throw Error("No Database url found in enviroment") +} + +export default defineConfig({ + out: './drizzle', + schema: './src/db/tables/index.ts', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_URL, + }, +}); \ No newline at end of file diff --git a/packages/backend/drizzle/0000_legal_the_fury.sql b/packages/backend/drizzle/0000_legal_the_fury.sql new file mode 100644 index 00000000..d5d044b4 --- /dev/null +++ b/packages/backend/drizzle/0000_legal_the_fury.sql @@ -0,0 +1,175 @@ +CREATE TABLE IF NOT EXISTS "organizations" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "systems" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "organization_id" text NOT NULL, + "system_model_id" text +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "keys" ( + "public_key" text PRIMARY KEY NOT NULL, + "private_key" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "readings" ( + "time" timestamp with time zone NOT NULL, + "systems_id" text NOT NULL, + "name" text NOT NULL, + "value" real NOT NULL, + "unit" text NOT NULL, + CONSTRAINT "readings_time_systems_id_name_pk" PRIMARY KEY("time","systems_id","name") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "systems_to_factory_areas" ( + "system_id" text NOT NULL, + "factory_area_id" text NOT NULL, + CONSTRAINT "systems_to_factory_areas_system_id_factory_area_id_pk" PRIMARY KEY("system_id","factory_area_id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "factory_areas" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "organization_id" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "system_models" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "parts_to_system_models" ( + "part_id" text NOT NULL, + "system_model_id" text NOT NULL, + CONSTRAINT "parts_to_system_models_part_id_system_model_id_pk" PRIMARY KEY("part_id","system_model_id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "parts" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "users" ( + "id" text PRIMARY KEY NOT NULL, + "is_superadmin" boolean DEFAULT false NOT NULL, + "microsoft_id" text +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "user_settings" ( + "id" text PRIMARY KEY NOT NULL, + "theme" text NOT NULL, + "product_updates" boolean NOT NULL, + "user_id" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "sessions" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "expires_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "users_to_factory_areas" ( + "user_id" text NOT NULL, + "factory_area_id" text NOT NULL, + CONSTRAINT "users_to_factory_areas_user_id_factory_area_id_pk" PRIMARY KEY("user_id","factory_area_id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "users_to_organizations" ( + "organization_id" text NOT NULL, + "user_id" text NOT NULL, + "role" text NOT NULL, + CONSTRAINT "users_to_organizations_organization_id_user_id_pk" PRIMARY KEY("organization_id","user_id") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "systems" ADD CONSTRAINT "systems_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "systems" ADD CONSTRAINT "systems_system_model_id_system_models_id_fk" FOREIGN KEY ("system_model_id") REFERENCES "public"."system_models"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "keys" ADD CONSTRAINT "keys_private_key_systems_id_fk" FOREIGN KEY ("private_key") REFERENCES "public"."systems"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "readings" ADD CONSTRAINT "readings_systems_id_systems_id_fk" FOREIGN KEY ("systems_id") REFERENCES "public"."systems"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "systems_to_factory_areas" ADD CONSTRAINT "systems_to_factory_areas_system_id_systems_id_fk" FOREIGN KEY ("system_id") REFERENCES "public"."systems"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "systems_to_factory_areas" ADD CONSTRAINT "systems_to_factory_areas_factory_area_id_factory_areas_id_fk" FOREIGN KEY ("factory_area_id") REFERENCES "public"."factory_areas"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "factory_areas" ADD CONSTRAINT "factory_areas_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "parts_to_system_models" ADD CONSTRAINT "parts_to_system_models_part_id_parts_id_fk" FOREIGN KEY ("part_id") REFERENCES "public"."parts"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "parts_to_system_models" ADD CONSTRAINT "parts_to_system_models_system_model_id_system_models_id_fk" FOREIGN KEY ("system_model_id") REFERENCES "public"."system_models"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "user_settings" ADD CONSTRAINT "user_settings_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "users_to_factory_areas" ADD CONSTRAINT "users_to_factory_areas_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "users_to_factory_areas" ADD CONSTRAINT "users_to_factory_areas_factory_area_id_factory_areas_id_fk" FOREIGN KEY ("factory_area_id") REFERENCES "public"."factory_areas"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "users_to_organizations" ADD CONSTRAINT "users_to_organizations_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "users_to_organizations" ADD CONSTRAINT "users_to_organizations_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/packages/backend/drizzle/0001_readings_hypertable.sql b/packages/backend/drizzle/0001_readings_hypertable.sql new file mode 100644 index 00000000..a669f871 --- /dev/null +++ b/packages/backend/drizzle/0001_readings_hypertable.sql @@ -0,0 +1,5 @@ +-- Custom SQL migration file, put you code below! -- + +-- Convert regular table to hypertable with multi-column partitioning +SELECT create_hypertable('readings', 'time', if_not_exists => TRUE); +SELECT add_dimension('readings', 'systems_id', number_partitions => 4, if_not_exists => TRUE); \ No newline at end of file diff --git a/packages/backend/drizzle/0002_long_invaders.sql b/packages/backend/drizzle/0002_long_invaders.sql new file mode 100644 index 00000000..3c72c949 --- /dev/null +++ b/packages/backend/drizzle/0002_long_invaders.sql @@ -0,0 +1,10 @@ +ALTER TABLE "readings" RENAME COLUMN "systems_id" TO "system_id";--> statement-breakpoint +ALTER TABLE "readings" DROP CONSTRAINT "readings_systems_id_systems_id_fk"; +--> statement-breakpoint +ALTER TABLE "readings" DROP CONSTRAINT "readings_time_systems_id_name_pk";--> statement-breakpoint +ALTER TABLE "readings" ADD CONSTRAINT "readings_time_system_id_name_pk" PRIMARY KEY("time","system_id","name");--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "readings" ADD CONSTRAINT "readings_system_id_systems_id_fk" FOREIGN KEY ("system_id") REFERENCES "public"."systems"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/packages/backend/drizzle/0003_flippant_mikhail_rasputin.sql b/packages/backend/drizzle/0003_flippant_mikhail_rasputin.sql new file mode 100644 index 00000000..01a73c9e --- /dev/null +++ b/packages/backend/drizzle/0003_flippant_mikhail_rasputin.sql @@ -0,0 +1,4 @@ +CREATE TYPE "public"."providers" AS ENUM('Gituhb', 'Microsoft');--> statement-breakpoint +ALTER TABLE "users" RENAME COLUMN "microsoft_id" TO "provider_id";--> statement-breakpoint +ALTER TABLE "users" ALTER COLUMN "provider_id" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "provider_name" "providers" NOT NULL; \ No newline at end of file diff --git a/packages/backend/drizzle/0004_far_lizard.sql b/packages/backend/drizzle/0004_far_lizard.sql new file mode 100644 index 00000000..5ecf7fab --- /dev/null +++ b/packages/backend/drizzle/0004_far_lizard.sql @@ -0,0 +1,4 @@ +ALTER TABLE "public"."users" ALTER COLUMN "provider_name" SET DATA TYPE text;--> statement-breakpoint +DROP TYPE "public"."providers";--> statement-breakpoint +CREATE TYPE "public"."providers" AS ENUM('Github', 'Microsoft');--> statement-breakpoint +ALTER TABLE "public"."users" ALTER COLUMN "provider_name" SET DATA TYPE "public"."providers" USING "provider_name"::"public"."providers"; \ No newline at end of file diff --git a/packages/backend/drizzle/meta/0000_snapshot.json b/packages/backend/drizzle/meta/0000_snapshot.json new file mode 100644 index 00000000..5393674d --- /dev/null +++ b/packages/backend/drizzle/meta/0000_snapshot.json @@ -0,0 +1,652 @@ +{ + "id": "b38be80d-55a4-42ad-a9b0-067873b2684e", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.systems": { + "name": "systems", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_model_id": { + "name": "system_model_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "systems_organization_id_organizations_id_fk": { + "name": "systems_organization_id_organizations_id_fk", + "tableFrom": "systems", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "systems_system_model_id_system_models_id_fk": { + "name": "systems_system_model_id_system_models_id_fk", + "tableFrom": "systems", + "tableTo": "system_models", + "columnsFrom": [ + "system_model_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.keys": { + "name": "keys", + "schema": "", + "columns": { + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "keys_private_key_systems_id_fk": { + "name": "keys_private_key_systems_id_fk", + "tableFrom": "keys", + "tableTo": "systems", + "columnsFrom": [ + "private_key" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.readings": { + "name": "readings", + "schema": "", + "columns": { + "time": { + "name": "time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "systems_id": { + "name": "systems_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "readings_systems_id_systems_id_fk": { + "name": "readings_systems_id_systems_id_fk", + "tableFrom": "readings", + "tableTo": "systems", + "columnsFrom": [ + "systems_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "readings_time_systems_id_name_pk": { + "name": "readings_time_systems_id_name_pk", + "columns": [ + "time", + "systems_id", + "name" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.systems_to_factory_areas": { + "name": "systems_to_factory_areas", + "schema": "", + "columns": { + "system_id": { + "name": "system_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "factory_area_id": { + "name": "factory_area_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "systems_to_factory_areas_system_id_systems_id_fk": { + "name": "systems_to_factory_areas_system_id_systems_id_fk", + "tableFrom": "systems_to_factory_areas", + "tableTo": "systems", + "columnsFrom": [ + "system_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "systems_to_factory_areas_factory_area_id_factory_areas_id_fk": { + "name": "systems_to_factory_areas_factory_area_id_factory_areas_id_fk", + "tableFrom": "systems_to_factory_areas", + "tableTo": "factory_areas", + "columnsFrom": [ + "factory_area_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "systems_to_factory_areas_system_id_factory_area_id_pk": { + "name": "systems_to_factory_areas_system_id_factory_area_id_pk", + "columns": [ + "system_id", + "factory_area_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.factory_areas": { + "name": "factory_areas", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "factory_areas_organization_id_organizations_id_fk": { + "name": "factory_areas_organization_id_organizations_id_fk", + "tableFrom": "factory_areas", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.system_models": { + "name": "system_models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.parts_to_system_models": { + "name": "parts_to_system_models", + "schema": "", + "columns": { + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_model_id": { + "name": "system_model_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "parts_to_system_models_part_id_parts_id_fk": { + "name": "parts_to_system_models_part_id_parts_id_fk", + "tableFrom": "parts_to_system_models", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_to_system_models_system_model_id_system_models_id_fk": { + "name": "parts_to_system_models_system_model_id_system_models_id_fk", + "tableFrom": "parts_to_system_models", + "tableTo": "system_models", + "columnsFrom": [ + "system_model_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "parts_to_system_models_part_id_system_model_id_pk": { + "name": "parts_to_system_models_part_id_system_model_id_pk", + "columns": [ + "part_id", + "system_model_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.parts": { + "name": "parts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "is_superadmin": { + "name": "is_superadmin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "microsoft_id": { + "name": "microsoft_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.user_settings": { + "name": "user_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_updates": { + "name": "product_updates", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_settings_user_id_users_id_fk": { + "name": "user_settings_user_id_users_id_fk", + "tableFrom": "user_settings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.users_to_factory_areas": { + "name": "users_to_factory_areas", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "factory_area_id": { + "name": "factory_area_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_factory_areas_user_id_users_id_fk": { + "name": "users_to_factory_areas_user_id_users_id_fk", + "tableFrom": "users_to_factory_areas", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users_to_factory_areas_factory_area_id_factory_areas_id_fk": { + "name": "users_to_factory_areas_factory_area_id_factory_areas_id_fk", + "tableFrom": "users_to_factory_areas", + "tableTo": "factory_areas", + "columnsFrom": [ + "factory_area_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_to_factory_areas_user_id_factory_area_id_pk": { + "name": "users_to_factory_areas_user_id_factory_area_id_pk", + "columns": [ + "user_id", + "factory_area_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.users_to_organizations": { + "name": "users_to_organizations", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_organizations_organization_id_organizations_id_fk": { + "name": "users_to_organizations_organization_id_organizations_id_fk", + "tableFrom": "users_to_organizations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users_to_organizations_user_id_users_id_fk": { + "name": "users_to_organizations_user_id_users_id_fk", + "tableFrom": "users_to_organizations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_to_organizations_organization_id_user_id_pk": { + "name": "users_to_organizations_organization_id_user_id_pk", + "columns": [ + "organization_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/backend/drizzle/meta/0001_snapshot.json b/packages/backend/drizzle/meta/0001_snapshot.json new file mode 100644 index 00000000..be2873b4 --- /dev/null +++ b/packages/backend/drizzle/meta/0001_snapshot.json @@ -0,0 +1,652 @@ +{ + "id": "4a91ac93-2700-4ef6-921a-a4390a3e328b", + "prevId": "b38be80d-55a4-42ad-a9b0-067873b2684e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.systems": { + "name": "systems", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_model_id": { + "name": "system_model_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "systems_organization_id_organizations_id_fk": { + "name": "systems_organization_id_organizations_id_fk", + "tableFrom": "systems", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "systems_system_model_id_system_models_id_fk": { + "name": "systems_system_model_id_system_models_id_fk", + "tableFrom": "systems", + "columnsFrom": [ + "system_model_id" + ], + "tableTo": "system_models", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.keys": { + "name": "keys", + "schema": "", + "columns": { + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "keys_private_key_systems_id_fk": { + "name": "keys_private_key_systems_id_fk", + "tableFrom": "keys", + "columnsFrom": [ + "private_key" + ], + "tableTo": "systems", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.readings": { + "name": "readings", + "schema": "", + "columns": { + "time": { + "name": "time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "systems_id": { + "name": "systems_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "readings_systems_id_systems_id_fk": { + "name": "readings_systems_id_systems_id_fk", + "tableFrom": "readings", + "columnsFrom": [ + "systems_id" + ], + "tableTo": "systems", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "readings_time_systems_id_name_pk": { + "name": "readings_time_systems_id_name_pk", + "columns": [ + "time", + "systems_id", + "name" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.systems_to_factory_areas": { + "name": "systems_to_factory_areas", + "schema": "", + "columns": { + "system_id": { + "name": "system_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "factory_area_id": { + "name": "factory_area_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "systems_to_factory_areas_system_id_systems_id_fk": { + "name": "systems_to_factory_areas_system_id_systems_id_fk", + "tableFrom": "systems_to_factory_areas", + "columnsFrom": [ + "system_id" + ], + "tableTo": "systems", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "systems_to_factory_areas_factory_area_id_factory_areas_id_fk": { + "name": "systems_to_factory_areas_factory_area_id_factory_areas_id_fk", + "tableFrom": "systems_to_factory_areas", + "columnsFrom": [ + "factory_area_id" + ], + "tableTo": "factory_areas", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "systems_to_factory_areas_system_id_factory_area_id_pk": { + "name": "systems_to_factory_areas_system_id_factory_area_id_pk", + "columns": [ + "system_id", + "factory_area_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.factory_areas": { + "name": "factory_areas", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "factory_areas_organization_id_organizations_id_fk": { + "name": "factory_areas_organization_id_organizations_id_fk", + "tableFrom": "factory_areas", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.system_models": { + "name": "system_models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.parts_to_system_models": { + "name": "parts_to_system_models", + "schema": "", + "columns": { + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_model_id": { + "name": "system_model_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "parts_to_system_models_part_id_parts_id_fk": { + "name": "parts_to_system_models_part_id_parts_id_fk", + "tableFrom": "parts_to_system_models", + "columnsFrom": [ + "part_id" + ], + "tableTo": "parts", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "parts_to_system_models_system_model_id_system_models_id_fk": { + "name": "parts_to_system_models_system_model_id_system_models_id_fk", + "tableFrom": "parts_to_system_models", + "columnsFrom": [ + "system_model_id" + ], + "tableTo": "system_models", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "parts_to_system_models_part_id_system_model_id_pk": { + "name": "parts_to_system_models_part_id_system_model_id_pk", + "columns": [ + "part_id", + "system_model_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.parts": { + "name": "parts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "is_superadmin": { + "name": "is_superadmin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "microsoft_id": { + "name": "microsoft_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.user_settings": { + "name": "user_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_updates": { + "name": "product_updates", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_settings_user_id_users_id_fk": { + "name": "user_settings_user_id_users_id_fk", + "tableFrom": "user_settings", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.users_to_factory_areas": { + "name": "users_to_factory_areas", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "factory_area_id": { + "name": "factory_area_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_factory_areas_user_id_users_id_fk": { + "name": "users_to_factory_areas_user_id_users_id_fk", + "tableFrom": "users_to_factory_areas", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "users_to_factory_areas_factory_area_id_factory_areas_id_fk": { + "name": "users_to_factory_areas_factory_area_id_factory_areas_id_fk", + "tableFrom": "users_to_factory_areas", + "columnsFrom": [ + "factory_area_id" + ], + "tableTo": "factory_areas", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "users_to_factory_areas_user_id_factory_area_id_pk": { + "name": "users_to_factory_areas_user_id_factory_area_id_pk", + "columns": [ + "user_id", + "factory_area_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.users_to_organizations": { + "name": "users_to_organizations", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_organizations_organization_id_organizations_id_fk": { + "name": "users_to_organizations_organization_id_organizations_id_fk", + "tableFrom": "users_to_organizations", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "users_to_organizations_user_id_users_id_fk": { + "name": "users_to_organizations_user_id_users_id_fk", + "tableFrom": "users_to_organizations", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "users_to_organizations_organization_id_user_id_pk": { + "name": "users_to_organizations_organization_id_user_id_pk", + "columns": [ + "organization_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "views": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/backend/drizzle/meta/0002_snapshot.json b/packages/backend/drizzle/meta/0002_snapshot.json new file mode 100644 index 00000000..5b29e33b --- /dev/null +++ b/packages/backend/drizzle/meta/0002_snapshot.json @@ -0,0 +1,652 @@ +{ + "id": "9cbd5568-7e71-41fd-89ae-486b15db9a03", + "prevId": "4a91ac93-2700-4ef6-921a-a4390a3e328b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.systems": { + "name": "systems", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_model_id": { + "name": "system_model_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "systems_organization_id_organizations_id_fk": { + "name": "systems_organization_id_organizations_id_fk", + "tableFrom": "systems", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "systems_system_model_id_system_models_id_fk": { + "name": "systems_system_model_id_system_models_id_fk", + "tableFrom": "systems", + "tableTo": "system_models", + "columnsFrom": [ + "system_model_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.keys": { + "name": "keys", + "schema": "", + "columns": { + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "keys_private_key_systems_id_fk": { + "name": "keys_private_key_systems_id_fk", + "tableFrom": "keys", + "tableTo": "systems", + "columnsFrom": [ + "private_key" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.readings": { + "name": "readings", + "schema": "", + "columns": { + "time": { + "name": "time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "system_id": { + "name": "system_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "readings_system_id_systems_id_fk": { + "name": "readings_system_id_systems_id_fk", + "tableFrom": "readings", + "tableTo": "systems", + "columnsFrom": [ + "system_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "readings_time_system_id_name_pk": { + "name": "readings_time_system_id_name_pk", + "columns": [ + "time", + "system_id", + "name" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.systems_to_factory_areas": { + "name": "systems_to_factory_areas", + "schema": "", + "columns": { + "system_id": { + "name": "system_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "factory_area_id": { + "name": "factory_area_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "systems_to_factory_areas_system_id_systems_id_fk": { + "name": "systems_to_factory_areas_system_id_systems_id_fk", + "tableFrom": "systems_to_factory_areas", + "tableTo": "systems", + "columnsFrom": [ + "system_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "systems_to_factory_areas_factory_area_id_factory_areas_id_fk": { + "name": "systems_to_factory_areas_factory_area_id_factory_areas_id_fk", + "tableFrom": "systems_to_factory_areas", + "tableTo": "factory_areas", + "columnsFrom": [ + "factory_area_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "systems_to_factory_areas_system_id_factory_area_id_pk": { + "name": "systems_to_factory_areas_system_id_factory_area_id_pk", + "columns": [ + "system_id", + "factory_area_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.factory_areas": { + "name": "factory_areas", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "factory_areas_organization_id_organizations_id_fk": { + "name": "factory_areas_organization_id_organizations_id_fk", + "tableFrom": "factory_areas", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.system_models": { + "name": "system_models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.parts_to_system_models": { + "name": "parts_to_system_models", + "schema": "", + "columns": { + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_model_id": { + "name": "system_model_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "parts_to_system_models_part_id_parts_id_fk": { + "name": "parts_to_system_models_part_id_parts_id_fk", + "tableFrom": "parts_to_system_models", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_to_system_models_system_model_id_system_models_id_fk": { + "name": "parts_to_system_models_system_model_id_system_models_id_fk", + "tableFrom": "parts_to_system_models", + "tableTo": "system_models", + "columnsFrom": [ + "system_model_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "parts_to_system_models_part_id_system_model_id_pk": { + "name": "parts_to_system_models_part_id_system_model_id_pk", + "columns": [ + "part_id", + "system_model_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.parts": { + "name": "parts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "is_superadmin": { + "name": "is_superadmin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "microsoft_id": { + "name": "microsoft_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.user_settings": { + "name": "user_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_updates": { + "name": "product_updates", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_settings_user_id_users_id_fk": { + "name": "user_settings_user_id_users_id_fk", + "tableFrom": "user_settings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.users_to_factory_areas": { + "name": "users_to_factory_areas", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "factory_area_id": { + "name": "factory_area_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_factory_areas_user_id_users_id_fk": { + "name": "users_to_factory_areas_user_id_users_id_fk", + "tableFrom": "users_to_factory_areas", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users_to_factory_areas_factory_area_id_factory_areas_id_fk": { + "name": "users_to_factory_areas_factory_area_id_factory_areas_id_fk", + "tableFrom": "users_to_factory_areas", + "tableTo": "factory_areas", + "columnsFrom": [ + "factory_area_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_to_factory_areas_user_id_factory_area_id_pk": { + "name": "users_to_factory_areas_user_id_factory_area_id_pk", + "columns": [ + "user_id", + "factory_area_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.users_to_organizations": { + "name": "users_to_organizations", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_organizations_organization_id_organizations_id_fk": { + "name": "users_to_organizations_organization_id_organizations_id_fk", + "tableFrom": "users_to_organizations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users_to_organizations_user_id_users_id_fk": { + "name": "users_to_organizations_user_id_users_id_fk", + "tableFrom": "users_to_organizations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_to_organizations_organization_id_user_id_pk": { + "name": "users_to_organizations_organization_id_user_id_pk", + "columns": [ + "organization_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/backend/drizzle/meta/0003_snapshot.json b/packages/backend/drizzle/meta/0003_snapshot.json new file mode 100644 index 00000000..040afb99 --- /dev/null +++ b/packages/backend/drizzle/meta/0003_snapshot.json @@ -0,0 +1,668 @@ +{ + "id": "9e16a5d7-58ce-4d47-ab61-35f564c2f71c", + "prevId": "9cbd5568-7e71-41fd-89ae-486b15db9a03", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.systems": { + "name": "systems", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_model_id": { + "name": "system_model_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "systems_organization_id_organizations_id_fk": { + "name": "systems_organization_id_organizations_id_fk", + "tableFrom": "systems", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "systems_system_model_id_system_models_id_fk": { + "name": "systems_system_model_id_system_models_id_fk", + "tableFrom": "systems", + "tableTo": "system_models", + "columnsFrom": [ + "system_model_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.keys": { + "name": "keys", + "schema": "", + "columns": { + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "keys_private_key_systems_id_fk": { + "name": "keys_private_key_systems_id_fk", + "tableFrom": "keys", + "tableTo": "systems", + "columnsFrom": [ + "private_key" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.readings": { + "name": "readings", + "schema": "", + "columns": { + "time": { + "name": "time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "system_id": { + "name": "system_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "readings_system_id_systems_id_fk": { + "name": "readings_system_id_systems_id_fk", + "tableFrom": "readings", + "tableTo": "systems", + "columnsFrom": [ + "system_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "readings_time_system_id_name_pk": { + "name": "readings_time_system_id_name_pk", + "columns": [ + "time", + "system_id", + "name" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.systems_to_factory_areas": { + "name": "systems_to_factory_areas", + "schema": "", + "columns": { + "system_id": { + "name": "system_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "factory_area_id": { + "name": "factory_area_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "systems_to_factory_areas_system_id_systems_id_fk": { + "name": "systems_to_factory_areas_system_id_systems_id_fk", + "tableFrom": "systems_to_factory_areas", + "tableTo": "systems", + "columnsFrom": [ + "system_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "systems_to_factory_areas_factory_area_id_factory_areas_id_fk": { + "name": "systems_to_factory_areas_factory_area_id_factory_areas_id_fk", + "tableFrom": "systems_to_factory_areas", + "tableTo": "factory_areas", + "columnsFrom": [ + "factory_area_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "systems_to_factory_areas_system_id_factory_area_id_pk": { + "name": "systems_to_factory_areas_system_id_factory_area_id_pk", + "columns": [ + "system_id", + "factory_area_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.factory_areas": { + "name": "factory_areas", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "factory_areas_organization_id_organizations_id_fk": { + "name": "factory_areas_organization_id_organizations_id_fk", + "tableFrom": "factory_areas", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.system_models": { + "name": "system_models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.parts_to_system_models": { + "name": "parts_to_system_models", + "schema": "", + "columns": { + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_model_id": { + "name": "system_model_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "parts_to_system_models_part_id_parts_id_fk": { + "name": "parts_to_system_models_part_id_parts_id_fk", + "tableFrom": "parts_to_system_models", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_to_system_models_system_model_id_system_models_id_fk": { + "name": "parts_to_system_models_system_model_id_system_models_id_fk", + "tableFrom": "parts_to_system_models", + "tableTo": "system_models", + "columnsFrom": [ + "system_model_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "parts_to_system_models_part_id_system_model_id_pk": { + "name": "parts_to_system_models_part_id_system_model_id_pk", + "columns": [ + "part_id", + "system_model_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.parts": { + "name": "parts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "is_superadmin": { + "name": "is_superadmin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_name": { + "name": "provider_name", + "type": "providers", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.user_settings": { + "name": "user_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_updates": { + "name": "product_updates", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_settings_user_id_users_id_fk": { + "name": "user_settings_user_id_users_id_fk", + "tableFrom": "user_settings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.users_to_factory_areas": { + "name": "users_to_factory_areas", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "factory_area_id": { + "name": "factory_area_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_factory_areas_user_id_users_id_fk": { + "name": "users_to_factory_areas_user_id_users_id_fk", + "tableFrom": "users_to_factory_areas", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users_to_factory_areas_factory_area_id_factory_areas_id_fk": { + "name": "users_to_factory_areas_factory_area_id_factory_areas_id_fk", + "tableFrom": "users_to_factory_areas", + "tableTo": "factory_areas", + "columnsFrom": [ + "factory_area_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_to_factory_areas_user_id_factory_area_id_pk": { + "name": "users_to_factory_areas_user_id_factory_area_id_pk", + "columns": [ + "user_id", + "factory_area_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.users_to_organizations": { + "name": "users_to_organizations", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_organizations_organization_id_organizations_id_fk": { + "name": "users_to_organizations_organization_id_organizations_id_fk", + "tableFrom": "users_to_organizations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users_to_organizations_user_id_users_id_fk": { + "name": "users_to_organizations_user_id_users_id_fk", + "tableFrom": "users_to_organizations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_to_organizations_organization_id_user_id_pk": { + "name": "users_to_organizations_organization_id_user_id_pk", + "columns": [ + "organization_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "enums": { + "public.providers": { + "name": "providers", + "schema": "public", + "values": [ + "Gituhb", + "Microsoft" + ] + } + }, + "schemas": {}, + "sequences": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/backend/drizzle/meta/0004_snapshot.json b/packages/backend/drizzle/meta/0004_snapshot.json new file mode 100644 index 00000000..3bf6c36b --- /dev/null +++ b/packages/backend/drizzle/meta/0004_snapshot.json @@ -0,0 +1,668 @@ +{ + "id": "ad376277-828b-49df-916c-38bfcbf0c5de", + "prevId": "9e16a5d7-58ce-4d47-ab61-35f564c2f71c", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.systems": { + "name": "systems", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_model_id": { + "name": "system_model_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "systems_organization_id_organizations_id_fk": { + "name": "systems_organization_id_organizations_id_fk", + "tableFrom": "systems", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "systems_system_model_id_system_models_id_fk": { + "name": "systems_system_model_id_system_models_id_fk", + "tableFrom": "systems", + "tableTo": "system_models", + "columnsFrom": [ + "system_model_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.keys": { + "name": "keys", + "schema": "", + "columns": { + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "keys_private_key_systems_id_fk": { + "name": "keys_private_key_systems_id_fk", + "tableFrom": "keys", + "tableTo": "systems", + "columnsFrom": [ + "private_key" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.readings": { + "name": "readings", + "schema": "", + "columns": { + "time": { + "name": "time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "system_id": { + "name": "system_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "readings_system_id_systems_id_fk": { + "name": "readings_system_id_systems_id_fk", + "tableFrom": "readings", + "tableTo": "systems", + "columnsFrom": [ + "system_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "readings_time_system_id_name_pk": { + "name": "readings_time_system_id_name_pk", + "columns": [ + "time", + "system_id", + "name" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.systems_to_factory_areas": { + "name": "systems_to_factory_areas", + "schema": "", + "columns": { + "system_id": { + "name": "system_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "factory_area_id": { + "name": "factory_area_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "systems_to_factory_areas_system_id_systems_id_fk": { + "name": "systems_to_factory_areas_system_id_systems_id_fk", + "tableFrom": "systems_to_factory_areas", + "tableTo": "systems", + "columnsFrom": [ + "system_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "systems_to_factory_areas_factory_area_id_factory_areas_id_fk": { + "name": "systems_to_factory_areas_factory_area_id_factory_areas_id_fk", + "tableFrom": "systems_to_factory_areas", + "tableTo": "factory_areas", + "columnsFrom": [ + "factory_area_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "systems_to_factory_areas_system_id_factory_area_id_pk": { + "name": "systems_to_factory_areas_system_id_factory_area_id_pk", + "columns": [ + "system_id", + "factory_area_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.factory_areas": { + "name": "factory_areas", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "factory_areas_organization_id_organizations_id_fk": { + "name": "factory_areas_organization_id_organizations_id_fk", + "tableFrom": "factory_areas", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.system_models": { + "name": "system_models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.parts_to_system_models": { + "name": "parts_to_system_models", + "schema": "", + "columns": { + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_model_id": { + "name": "system_model_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "parts_to_system_models_part_id_parts_id_fk": { + "name": "parts_to_system_models_part_id_parts_id_fk", + "tableFrom": "parts_to_system_models", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_to_system_models_system_model_id_system_models_id_fk": { + "name": "parts_to_system_models_system_model_id_system_models_id_fk", + "tableFrom": "parts_to_system_models", + "tableTo": "system_models", + "columnsFrom": [ + "system_model_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "parts_to_system_models_part_id_system_model_id_pk": { + "name": "parts_to_system_models_part_id_system_model_id_pk", + "columns": [ + "part_id", + "system_model_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.parts": { + "name": "parts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "is_superadmin": { + "name": "is_superadmin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_name": { + "name": "provider_name", + "type": "providers", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.user_settings": { + "name": "user_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_updates": { + "name": "product_updates", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_settings_user_id_users_id_fk": { + "name": "user_settings_user_id_users_id_fk", + "tableFrom": "user_settings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.users_to_factory_areas": { + "name": "users_to_factory_areas", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "factory_area_id": { + "name": "factory_area_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_factory_areas_user_id_users_id_fk": { + "name": "users_to_factory_areas_user_id_users_id_fk", + "tableFrom": "users_to_factory_areas", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users_to_factory_areas_factory_area_id_factory_areas_id_fk": { + "name": "users_to_factory_areas_factory_area_id_factory_areas_id_fk", + "tableFrom": "users_to_factory_areas", + "tableTo": "factory_areas", + "columnsFrom": [ + "factory_area_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_to_factory_areas_user_id_factory_area_id_pk": { + "name": "users_to_factory_areas_user_id_factory_area_id_pk", + "columns": [ + "user_id", + "factory_area_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "public.users_to_organizations": { + "name": "users_to_organizations", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_organizations_organization_id_organizations_id_fk": { + "name": "users_to_organizations_organization_id_organizations_id_fk", + "tableFrom": "users_to_organizations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users_to_organizations_user_id_users_id_fk": { + "name": "users_to_organizations_user_id_users_id_fk", + "tableFrom": "users_to_organizations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_to_organizations_organization_id_user_id_pk": { + "name": "users_to_organizations_organization_id_user_id_pk", + "columns": [ + "organization_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "enums": { + "public.providers": { + "name": "providers", + "schema": "public", + "values": [ + "Github", + "Microsoft" + ] + } + }, + "schemas": {}, + "sequences": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/backend/drizzle/meta/_journal.json b/packages/backend/drizzle/meta/_journal.json new file mode 100644 index 00000000..af34518f --- /dev/null +++ b/packages/backend/drizzle/meta/_journal.json @@ -0,0 +1,41 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1730801517377, + "tag": "0000_legal_the_fury", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1730801858101, + "tag": "0001_readings_hypertable", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1731069745244, + "tag": "0002_long_invaders", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1732018032091, + "tag": "0003_flippant_mikhail_rasputin", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1732020425993, + "tag": "0004_far_lizard", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts new file mode 100644 index 00000000..e74fc037 --- /dev/null +++ b/packages/backend/src/index.ts @@ -0,0 +1,22 @@ +import { logger } from "@bogeychan/elysia-logger"; +import { Elysia, t } from "elysia"; + +const api = new Elysia({ prefix: "/api" }).get( + "/hello", + ({ query }) => `Hello ${query.name}`, + { + query: t.Object({ + name: t.String(), + }), + }, +); + +const app = new Elysia() + .use(logger()) + .use(api); + +app.listen(process.env.PORT as string, () => + console.log(`🦊 Server started at ${app.server?.url.origin}`), +); + +export type App = typeof app; diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json new file mode 100644 index 00000000..13ae0b70 --- /dev/null +++ b/packages/backend/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "lib": [ + "ESNext" + ], + "module": "ESNext", + "target": "ESNext", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "rootDir": "./src", + "noEmit": true + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/frontend/.dockerignore b/packages/frontend/.dockerignore new file mode 100644 index 00000000..a5ddbb57 --- /dev/null +++ b/packages/frontend/.dockerignore @@ -0,0 +1,8 @@ +node_modules* + +# Output +.output +.vercel +/.svelte-kit +/build +./build diff --git a/packages/frontend/.gitignore b/packages/frontend/.gitignore new file mode 100644 index 00000000..79518f71 --- /dev/null +++ b/packages/frontend/.gitignore @@ -0,0 +1,21 @@ +node_modules + +# Output +.output +.vercel +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/packages/frontend/.npmrc b/packages/frontend/.npmrc new file mode 100644 index 00000000..b6f27f13 --- /dev/null +++ b/packages/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/packages/frontend/.prettierignore b/packages/frontend/.prettierignore new file mode 100644 index 00000000..ab78a95d --- /dev/null +++ b/packages/frontend/.prettierignore @@ -0,0 +1,4 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/packages/frontend/.prettierrc b/packages/frontend/.prettierrc new file mode 100644 index 00000000..3f7802c3 --- /dev/null +++ b/packages/frontend/.prettierrc @@ -0,0 +1,15 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/packages/frontend/Caddyfile b/packages/frontend/Caddyfile new file mode 100644 index 00000000..aeec606a --- /dev/null +++ b/packages/frontend/Caddyfile @@ -0,0 +1,11 @@ +{ + servers { + trusted_proxies static private_ranges + } +} + +:5173 { + root * /usr/share/caddy + file_server + try_files {path} /200.html +} \ No newline at end of file diff --git a/packages/frontend/README.md b/packages/frontend/README.md new file mode 100644 index 00000000..d591477e --- /dev/null +++ b/packages/frontend/README.md @@ -0,0 +1,21 @@ +# Single Page Application with sveltekit + +This project is made to work in a monorepo with elysia and sveltekit. + +For development purposes please see the [README.MD](../../README.MD) file. + +## Developing + +```bash +bun dev +``` + +## Building + +To create a production version of your app: + +```bash +bun run build +``` + +You can preview the production build with `bun preview`. diff --git a/packages/frontend/develop.dockerfile b/packages/frontend/develop.dockerfile new file mode 100644 index 00000000..c738192b --- /dev/null +++ b/packages/frontend/develop.dockerfile @@ -0,0 +1,26 @@ +# Use Bun (JavaScript runtime) image as the base +FROM oven/bun:1 AS build + +# Set the working directory to /app inside the container +WORKDIR /app + +# Copy necessary package files to install dependencies +COPY bun.lockb package.json /app/ +COPY /packages/backend/package.json /app/packages/backend/ +COPY /packages/frontend/package.json /app/packages/frontend/ + +# Install dependencies +RUN bun install + +# Copy the rest of your app's source code +COPY /packages/frontend ./packages/frontend + +# Move directory to frontend +WORKDIR /app/packages/frontend + +# Build your app +RUN bun run build + +CMD ["bunx", "vite", "dev"] + +EXPOSE 5173 \ No newline at end of file diff --git a/packages/frontend/dockerfile b/packages/frontend/dockerfile new file mode 100644 index 00000000..ba5dce34 --- /dev/null +++ b/packages/frontend/dockerfile @@ -0,0 +1,38 @@ +# Stage 1: Build the application +FROM oven/bun AS build + +WORKDIR /app + +# Cache packages +COPY package.json package.json +COPY bun.lockb bun.lockb + +COPY /packages/backend/package.json ./packages/backend/package.json +COPY /packages/frontend/package.json ./packages/frontend/package.json + +# Install dependencies +RUN bun install + +# Copy the rest of your app's source code +COPY /packages/frontend ./packages/frontend + +# Move directory to frontend +WORKDIR /app/packages/frontend + +# Build your app +RUN bun run build + +# Stage 2: Serve the application with Caddy +FROM caddy:2-alpine + +# Copy the built assets from the build stage +COPY --from=build /app/packages/frontend/build /usr/share/caddy + +# Copy your Caddyfile +COPY ./packages/frontend/Caddyfile /etc/caddy/Caddyfile + +# Expose port 8080 HTTP +EXPOSE 8080 + +# Start Caddy +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] \ No newline at end of file diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js new file mode 100644 index 00000000..a5265658 --- /dev/null +++ b/packages/frontend/eslint.config.js @@ -0,0 +1,33 @@ +import prettier from 'eslint-config-prettier'; +import js from '@eslint/js'; +import svelte from 'eslint-plugin-svelte'; +import globals from 'globals'; +import ts from 'typescript-eslint'; + +export default ts.config( + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs['flat/recommended'], + prettier, + ...svelte.configs['flat/prettier'], + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node + } + } + }, + { + files: ['**/*.svelte'], + + languageOptions: { + parserOptions: { + parser: ts.parser + } + } + }, + { + ignores: ['build/', '.svelte-kit/', 'dist/'] + } +); diff --git a/packages/frontend/src/app.d.ts b/packages/frontend/src/app.d.ts new file mode 100644 index 00000000..da08e6da --- /dev/null +++ b/packages/frontend/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/packages/frontend/src/app.html b/packages/frontend/src/app.html new file mode 100644 index 00000000..77a5ff52 --- /dev/null +++ b/packages/frontend/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/packages/frontend/src/lib/index.ts b/packages/frontend/src/lib/index.ts new file mode 100644 index 00000000..856f2b6c --- /dev/null +++ b/packages/frontend/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/packages/frontend/src/routes/+layout.ts b/packages/frontend/src/routes/+layout.ts new file mode 100644 index 00000000..83addb7e --- /dev/null +++ b/packages/frontend/src/routes/+layout.ts @@ -0,0 +1,2 @@ +export const ssr = false; +export const prerender = false; diff --git a/packages/frontend/src/routes/+page.svelte b/packages/frontend/src/routes/+page.svelte new file mode 100644 index 00000000..bbd26ba8 --- /dev/null +++ b/packages/frontend/src/routes/+page.svelte @@ -0,0 +1,34 @@ + + + + + + +{#await response} +

Loading...

+{:then { data, error }} + {#if error} +

Error response: {error.value}

+ {:else} +

Yippy. I got the following result: {data}

+ {/if} +{:catch error} +

{error.message}

+{/await} diff --git a/packages/frontend/static/favicon.png b/packages/frontend/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..825b9e65af7c104cfb07089bb28659393b4f2097 GIT binary patch literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH Date: Mon, 25 Nov 2024 23:34:28 +0100 Subject: [PATCH 002/341] Add backend --- packages/backend/src/auth/login/github.ts | 82 ++++++++ packages/backend/src/auth/login/microsoft.ts | 100 ++++++++++ packages/backend/src/auth/logout.ts | 21 ++ packages/backend/src/auth/lucia.ts | 79 ++++++++ packages/backend/src/auth/middleware.ts | 30 +++ packages/backend/src/auth/routes.ts | 9 + packages/backend/src/auth/status.ts | 6 + packages/backend/src/db/migrate.ts | 7 + packages/backend/src/db/model.ts | 117 +++++++++++ packages/backend/src/db/postgres.ts | 4 + packages/backend/src/db/seed.ts | 88 +++++++++ .../src/db/tables/factory_areas/schema.ts | 24 +++ packages/backend/src/db/tables/index.ts | 17 ++ .../backend/src/db/tables/keys/queries.ts | 16 ++ packages/backend/src/db/tables/keys/schema.ts | 24 +++ .../src/db/tables/organizations/schema.ts | 16 ++ .../backend/src/db/tables/parts/schema.ts | 16 ++ .../tables/parts_to_system_models/schema.ts | 17 ++ .../src/db/tables/readings/api.test.ts | 181 ++++++++++++++++++ .../backend/src/db/tables/readings/api.ts | 72 +++++++ .../backend/src/db/tables/readings/queries.ts | 33 ++++ .../backend/src/db/tables/readings/schema.ts | 30 +++ .../backend/src/db/tables/sessions/queries.ts | 24 +++ .../backend/src/db/tables/sessions/schema.ts | 23 +++ .../src/db/tables/system_models/schema.ts | 17 ++ .../backend/src/db/tables/systems/schema.ts | 25 +++ .../tables/systems_to_factory_areas/schema.ts | 18 ++ .../src/db/tables/user_settings/schema.ts | 23 +++ .../backend/src/db/tables/users/queries.ts | 21 ++ .../backend/src/db/tables/users/schema.ts | 23 +++ .../tables/users_to_factory_areas/schema.ts | 17 ++ .../tables/users_to_organizations/schema.ts | 18 ++ packages/backend/src/db/timescale.ts | 32 ++++ packages/backend/src/db/utils.ts | 97 ++++++++++ packages/backend/src/environment.ts | 45 +++++ packages/backend/src/types/errors.ts | 18 ++ packages/backend/src/types/strict.ts | 27 +++ 37 files changed, 1417 insertions(+) create mode 100644 packages/backend/src/auth/login/github.ts create mode 100644 packages/backend/src/auth/login/microsoft.ts create mode 100644 packages/backend/src/auth/logout.ts create mode 100644 packages/backend/src/auth/lucia.ts create mode 100644 packages/backend/src/auth/middleware.ts create mode 100644 packages/backend/src/auth/routes.ts create mode 100644 packages/backend/src/auth/status.ts create mode 100644 packages/backend/src/db/migrate.ts create mode 100644 packages/backend/src/db/model.ts create mode 100644 packages/backend/src/db/postgres.ts create mode 100644 packages/backend/src/db/seed.ts create mode 100644 packages/backend/src/db/tables/factory_areas/schema.ts create mode 100644 packages/backend/src/db/tables/index.ts create mode 100644 packages/backend/src/db/tables/keys/queries.ts create mode 100644 packages/backend/src/db/tables/keys/schema.ts create mode 100644 packages/backend/src/db/tables/organizations/schema.ts create mode 100644 packages/backend/src/db/tables/parts/schema.ts create mode 100644 packages/backend/src/db/tables/parts_to_system_models/schema.ts create mode 100644 packages/backend/src/db/tables/readings/api.test.ts create mode 100644 packages/backend/src/db/tables/readings/api.ts create mode 100644 packages/backend/src/db/tables/readings/queries.ts create mode 100644 packages/backend/src/db/tables/readings/schema.ts create mode 100644 packages/backend/src/db/tables/sessions/queries.ts create mode 100644 packages/backend/src/db/tables/sessions/schema.ts create mode 100644 packages/backend/src/db/tables/system_models/schema.ts create mode 100644 packages/backend/src/db/tables/systems/schema.ts create mode 100644 packages/backend/src/db/tables/systems_to_factory_areas/schema.ts create mode 100644 packages/backend/src/db/tables/user_settings/schema.ts create mode 100644 packages/backend/src/db/tables/users/queries.ts create mode 100644 packages/backend/src/db/tables/users/schema.ts create mode 100644 packages/backend/src/db/tables/users_to_factory_areas/schema.ts create mode 100644 packages/backend/src/db/tables/users_to_organizations/schema.ts create mode 100644 packages/backend/src/db/timescale.ts create mode 100644 packages/backend/src/db/utils.ts create mode 100644 packages/backend/src/environment.ts create mode 100644 packages/backend/src/types/errors.ts create mode 100644 packages/backend/src/types/strict.ts diff --git a/packages/backend/src/auth/login/github.ts b/packages/backend/src/auth/login/github.ts new file mode 100644 index 00000000..50cdce7a --- /dev/null +++ b/packages/backend/src/auth/login/github.ts @@ -0,0 +1,82 @@ +import { generateState } from "arctic"; +import { GitHub } from "arctic"; +import { type Cookie, Elysia, error, redirect, t } from "elysia"; +import { Queries, Schema } from "../../db/model"; +import { environment } from "../../environment"; +import { catchError } from "../../types/errors"; +import { createSession, generateSessionToken, setSessionTokenCookie } from "../lucia"; + +export const github = new GitHub(environment.GITHUB_CLIENT_ID, environment.GITHUB_CLIENT_SECRET, null); + +export const githubRoute = new Elysia() + .get( + "/github", + ({ cookie: { githubState } }) => { + const state = generateState(); + const url = github.createAuthorizationURL(state, []); + + githubState.value = state; + + return redirect(url.toString(), 302); + }, + { + cookie: t.Partial(Schema.cookie.github), + }, + ) + .get( + "/github/callback", + async ({ query: { code, state }, cookie: { githubState, sessionId } }) => { + if (state !== githubState.value) { + return error(400); + } + + const [err, tokens] = await catchError(github.validateAuthorizationCode(code)); + if (err) { + return error(400, err); + } + + const githubUserResponse = await fetch("https://api.github.com/user", { + headers: { + Authorization: `Bearer ${tokens.accessToken()}`, + }, + }); + + const { id: githubId, login: githubUsername } = (await githubUserResponse.json()) as { + id?: string; + login?: string; + }; + + if (!githubId || !githubUsername) { + return error(500, "Not able to parse github id and login"); + } + + const existingUser = await Queries.users.selectUniqueWithProvider({ + provider_name: "Github", + provider_id: githubId, + }); + + if (existingUser) { + const sessionToken = generateSessionToken(); + const session = await createSession(sessionToken, existingUser.id); + + setSessionTokenCookie(sessionId, sessionToken, session.expires_at); + + return redirect("/api/status", 302); + } + + const user = await Queries.users.create({ provider_name: "Github", provider_id: githubId }); + + const sessionToken = generateSessionToken(); + const session = await createSession(sessionToken, user.id); + setSessionTokenCookie(sessionId, sessionToken, session.expires_at); + + return redirect("/api/status", 302); + }, + { + query: t.Object({ + code: t.String(), + state: t.String(), + }), + cookie: Schema.cookie.github, + }, + ); diff --git a/packages/backend/src/auth/login/microsoft.ts b/packages/backend/src/auth/login/microsoft.ts new file mode 100644 index 00000000..0d31fd6a --- /dev/null +++ b/packages/backend/src/auth/login/microsoft.ts @@ -0,0 +1,100 @@ +import { decodeIdToken, generateCodeVerifier, generateState } from "arctic"; +import { MicrosoftEntraId } from "arctic"; +import { type Cookie, Elysia, Static, error, redirect, t } from "elysia"; +import { TypeCompiler } from "elysia/type-system"; +import { Queries, Schema } from "../../db/model"; +import { environment } from "../../environment"; +import { catchError } from "../../types/errors"; +import { createSession, generateSessionToken, setSessionTokenCookie } from "../lucia"; + +export const entraId = new MicrosoftEntraId( + environment.MICROSOFT_TENANT_ID, + environment.MICROSOFT_CLIENT_ID, + environment.MICROSOFT_CLIENT_SECRET, + environment.MICROSOFT_REDIRECT_URI, +); + +export const microsoftRoute = new Elysia() + .get( + "/microsoft", + ({ cookie }) => { + const state = generateState(); + const codeVerifier = generateCodeVerifier(); + const scopes = ["openid", "profile"]; + + const url = entraId.createAuthorizationURL(state, codeVerifier, scopes); + + cookie.microsoftState.value = state; + cookie.microsoftCode.value = codeVerifier; + + return redirect(url.toString(), 302); + }, + { + cookie: t.Partial(Schema.cookie.microsoft), + }, + ) + .get( + "/microsoft/callback", + async ({ query: { code, state }, cookie }) => { + // Verify that the state is the same as the one we set in the cookie + if (state !== cookie.microsoftState.value) { + return error(400); + } + + // Call the microsoft API to get validate authorization code + const codeVerifier = cookie.microsoftCode.value; + + const [err, tokens] = await catchError(entraId.validateAuthorizationCode(code, codeVerifier)); + if (err) { + return error(400, err); + } + + // Call the microsoft API to get user info + const userResponse = await fetch("https://graph.microsoft.com/oidc/userinfo", { + headers: { + Authorization: `Bearer ${tokens.accessToken()}`, + }, + }).then((r) => r.json()); + + const userParsed = validateUser.Decode(userResponse); + + const existingUser = await Queries.users.selectUniqueWithProvider({ + provider_name: "Microsoft", + provider_id: userParsed.sub, + }); + + if (existingUser) { + const sessionToken = generateSessionToken(); + const session = await createSession(sessionToken, existingUser.id); + + setSessionTokenCookie(cookie.sessionId, sessionToken, session.expires_at); + + return redirect("/api/status", 302); + } + + const user = await Queries.users.create({ provider_name: "Microsoft", provider_id: userParsed.sub }); + + const sessionToken = generateSessionToken(); + const session = await createSession(sessionToken, user.id); + setSessionTokenCookie(cookie.sessionId, sessionToken, session.expires_at); + + return redirect("/api/status", 302); + }, + { + query: t.Object({ + code: t.String(), + state: t.String(), + }), + cookie: Schema.cookie.microsoft, + }, + ); + +const UserSchema = t.Object({ + sub: t.String(), + name: t.String(), + family_name: t.String(), + given_name: t.String(), + picture: t.String(), +}); + +const validateUser = TypeCompiler.Compile(UserSchema); diff --git a/packages/backend/src/auth/logout.ts b/packages/backend/src/auth/logout.ts new file mode 100644 index 00000000..1ac3b3a6 --- /dev/null +++ b/packages/backend/src/auth/logout.ts @@ -0,0 +1,21 @@ +import Elysia, { error, redirect, t } from "elysia"; +import { Schema } from "../db/model"; +import { Authenticate, deleteSessionTokenCookie, invalidateSession } from "./lucia"; + +export const logoutRoutes = new Elysia().get( + "/logout", + async ({ cookie: { sessionId } }) => { + const { session } = await Authenticate(sessionId); + + if (!session) { + return error(401); + } + + await invalidateSession(session.id); + deleteSessionTokenCookie(sessionId); + return redirect("/api/status", 302); + }, + { + cookie: Schema.cookie.session, + }, +); diff --git a/packages/backend/src/auth/lucia.ts b/packages/backend/src/auth/lucia.ts new file mode 100644 index 00000000..ffb7fb3f --- /dev/null +++ b/packages/backend/src/auth/lucia.ts @@ -0,0 +1,79 @@ +import { sha256 } from "@oslojs/crypto/sha2"; +import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from "@oslojs/encoding"; +import { t } from "elysia"; +import type { Cookie } from "elysia/cookies"; +import { Queries } from "../db/model"; +import type { Session, User } from "../db/tables"; +import { environment } from "../environment"; + +export function generateSessionToken(): string { + const bytes = new Uint8Array(20); + crypto.getRandomValues(bytes); + const token = encodeBase32LowerCaseNoPadding(bytes); + return token; +} + +export async function createSession(token: string, user_id: string): Promise { + const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + const session: Session = { + id: sessionId, + user_id, + expires_at: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), + }; + await Queries.sessions.create(session); + return session; +} + +export type SessionValidationResult = { session: Session; user: User } | { session: null; user: null }; +export async function validateSessionToken(token: string): Promise { + const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + const result = await Queries.sessions.selectWithUser(sessionId); + + if (!result) { + return { session: null, user: null }; + } + + const { session, user } = result; + if (Date.now() >= session.expires_at.getTime()) { + await Queries.sessions.delete(sessionId); + return { session: null, user: null }; + } + if (Date.now() >= session.expires_at.getTime() - 1000 * 60 * 60 * 24 * 15) { + session.expires_at = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30); + await Queries.sessions.update(session); + } + return { session, user }; +} + +export async function invalidateSession(sessionId: string): Promise { + await Queries.sessions.delete(sessionId); +} + +export function setSessionTokenCookie(cookie: Cookie, sessionToken: string, expiresAt?: Date) { + cookie.cookie = { + value: sessionToken, + httpOnly: true, + sameSite: "lax", + secure: environment.PROD, + expires: expiresAt, + maxAge: 60 * 10, + path: "/", + }; +} + +export function deleteSessionTokenCookie(cookie: Cookie) { + cookie.remove(); +} + +export async function Authenticate(cookie: Cookie): Promise { + const token = cookie.value; + + const { session, user } = await validateSessionToken(token); + if (session === null || user === null) { + deleteSessionTokenCookie(cookie); + return { session: null, user: null }; + } + + setSessionTokenCookie(cookie, token, session.expires_at); + return { session, user }; +} diff --git a/packages/backend/src/auth/middleware.ts b/packages/backend/src/auth/middleware.ts new file mode 100644 index 00000000..3cb9c8cb --- /dev/null +++ b/packages/backend/src/auth/middleware.ts @@ -0,0 +1,30 @@ +import Elysia from "elysia"; +import { Schema } from "../db/model"; +import type { Session, User } from "../db/tables"; +import { setSessionTokenCookie, validateSessionToken } from "./lucia"; + +export const AuthService = new Elysia({ name: "Service.Auth" }) + .guard({ + cookie: Schema.cookie.session, + }) + .resolve(async ({ cookie: { sessionId } }) => { + const { user, session } = await validateSessionToken(sessionId.value); + return { user, session }; + }) + .onBeforeHandle(({ user, session, error, cookie: { sessionId } }) => { + if (!user || !session) { + sessionId.remove(); + return error("Unauthorized", "The provided sessionId is invalid"); + } + }) + .resolve(({ user, session, cookie: { sessionId } }) => { + // We can safely cast the types here because + // sessionId is validated in the previous onBeforeHandle hook + user = user as User; + session = session as Session; + + setSessionTokenCookie(sessionId, sessionId.value, session.expires_at); + + return { user, session }; + }) + .as("plugin"); diff --git a/packages/backend/src/auth/routes.ts b/packages/backend/src/auth/routes.ts new file mode 100644 index 00000000..5fd406ff --- /dev/null +++ b/packages/backend/src/auth/routes.ts @@ -0,0 +1,9 @@ +import Elysia from "elysia"; + +import { githubRoute } from "./login/github"; +import { microsoftRoute } from "./login/microsoft"; +import { logoutRoutes } from "./logout"; +import { statusRoutes } from "./status"; +const loginRoutes = new Elysia({ prefix: "/login" }).use(githubRoute).use(microsoftRoute); + +export const authRoutes = new Elysia().use(loginRoutes).use(logoutRoutes).use(statusRoutes); diff --git a/packages/backend/src/auth/status.ts b/packages/backend/src/auth/status.ts new file mode 100644 index 00000000..bbbecb56 --- /dev/null +++ b/packages/backend/src/auth/status.ts @@ -0,0 +1,6 @@ +import Elysia from "elysia"; +import { AuthService } from "./middleware"; + +export const statusRoutes = new Elysia() + .use(AuthService) + .get("/status", ({ user }) => `You are authenticated with ${user.provider_name} as user: ${user.provider_id}`); diff --git a/packages/backend/src/db/migrate.ts b/packages/backend/src/db/migrate.ts new file mode 100644 index 00000000..e5dd4c0d --- /dev/null +++ b/packages/backend/src/db/migrate.ts @@ -0,0 +1,7 @@ +import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; +import { migrate } from "drizzle-orm/postgres-js/migrator"; +import { db } from "./postgres"; + +export async function runMigrations() { + return migrate(db, { migrationsFolder: "./drizzle" }); +} diff --git a/packages/backend/src/db/model.ts b/packages/backend/src/db/model.ts new file mode 100644 index 00000000..464dd18a --- /dev/null +++ b/packages/backend/src/db/model.ts @@ -0,0 +1,117 @@ +import { t } from "elysia"; +import { github } from "../auth/login/github"; +import { + factoryAreas, + insertFactoryAreaSchema, + insertKeysSchema, + insertOrganizationsSchema, + insertPartsSchema, + insertReadingsSchema, + insertSessionsSchema, + insertSystemModelsSchema, + insertSystemsSchema, + insertUserSchema, + insertUserSettingsSchema, + keys, + organizations, + parts, + partsToSystemModels, + readings, + selectKeysSchema, + selectReadingsSchema, + sessions, + systemModels, + systems, + systemsToFactoryAreas, + userSettings, + users, + usersToFactoryAreas, + usersToOrganizations, +} from "./tables"; +import { keysQueries } from "./tables/keys/queries"; +import { readingsQueries } from "./tables/readings/queries"; +import { sessionQueries } from "./tables/sessions/queries"; +import { usersQueries } from "./tables/users/queries"; +import { spreads } from "./utils"; + +export const Table = { + organizations, + systems, + keys, + readings, + systemsToFactoryAreas, + factoryAreas, + systemModels, + partsToSystemModels, + parts, + users, + userSettings, + sessions, + usersToOrganizations, + usersToFactoryAreas, +} as const; + +export const Schema = { + insert: spreads( + { + users: insertUserSchema, + user_settings: insertUserSettingsSchema, + sessions: insertSessionsSchema, + organizations: insertOrganizationsSchema, + factoryAreas: insertFactoryAreaSchema, + systems: insertSystemsSchema, + systemModels: insertSystemModelsSchema, + readings: insertReadingsSchema, + parts: insertPartsSchema, + keys: insertKeysSchema, + }, + "insert", + ), + select: spreads( + { + keys: selectKeysSchema, + readings: selectReadingsSchema, + }, + "select", + ), + cookie: { + session: t.Cookie({ + sessionId: t.String({ + description: "Session cookie will be used to keep a user logged in", + error:{error: "Unauthorized access, due to invalid session cookie"}, + }), + }, ), + github: t.Cookie( + { + githubState: t.String(), + }, + { + path: "/", + httpOnly: true, + maxAge: 60 * 10, + sameSite: "lax", + }, + ), + microsoft: t.Cookie( + { + microsoftState: t.String(), + microsoftCode: t.String(), + }, + { + path: "/", + httpOnly: true, + maxAge: 60 * 10, + sameSite: "lax", + }, + ), + }, +}; + +const testCookie = t.Required(Schema.cookie.github, {}); + +export const Queries = { + keys: keysQueries, + readings: readingsQueries, + sessions: sessionQueries, + users: usersQueries, +}; diff --git a/packages/backend/src/db/postgres.ts b/packages/backend/src/db/postgres.ts new file mode 100644 index 00000000..7bf15b9d --- /dev/null +++ b/packages/backend/src/db/postgres.ts @@ -0,0 +1,4 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import { environment } from "../environment"; + +export const db = drizzle(environment.DATABASE_URL, { casing: "snake_case" }); diff --git a/packages/backend/src/db/seed.ts b/packages/backend/src/db/seed.ts new file mode 100644 index 00000000..6660d3a1 --- /dev/null +++ b/packages/backend/src/db/seed.ts @@ -0,0 +1,88 @@ +import { Table } from "./model"; +import { db } from "./postgres"; + +export async function seedDatabase() { + // Start a transaction + const result = await db.transaction(async (tx) => { + // Insert organization + const organization = await tx + .insert(Table.organizations) + .values([{ name: "Trivision" }]) + .returning() + .then((v) => v.at(0)); + + if (!organization) { + throw new Error("Failed to insert organization"); + } + + // Insert system + const system = await tx + .insert(Table.systems) + .values([ + { + name: "VisioPointer", + organization_id: organization.id, + }, + ]) + .returning() + .then((v) => v.at(0)); + + if (!system) { + throw new Error("Failed to insert system"); + } + + // Insert key + const key = await tx + .insert(Table.keys) + .values([{ private_key: system.id }]) + .returning() + .then((v) => v.at(0)); + + if (!key) { + throw new Error("Failed to insert key"); + } + + // Insert readings. + const readings = await tx + .insert(Table.readings) + .values([ + { + name: "cpu temperature", + time: new Date(), + unit: "C", + value: 40, + system_id: system.id, + }, + { + name: "cpu usage", + time: new Date(), + unit: "%", + value: 20, + system_id: system.id, + }, + { + name: "disk usage", + time: new Date(), + unit: "%", + value: 95, + system_id: system.id, + }, + ]) + .returning(); + + return { organization, system, key, readings }; + }); + + return result; +} + +/* Execute the seeding +try { + const result = await seedDatabase(); + console.log("Database seeded successfully:", result); + exit(0); +} catch (error) { + console.error("Failed to seed database:", error); + throw error; +} +*/ diff --git a/packages/backend/src/db/tables/factory_areas/schema.ts b/packages/backend/src/db/tables/factory_areas/schema.ts new file mode 100644 index 00000000..485db96b --- /dev/null +++ b/packages/backend/src/db/tables/factory_areas/schema.ts @@ -0,0 +1,24 @@ +import { pgTable, text, uuid } from "drizzle-orm/pg-core"; +import { createInsertSchema } from "drizzle-typebox"; +import { t } from "elysia"; +import { organizations } from ".."; +import { generateRandomString } from "../../utils"; + +const LENGTH = 12; + +export const factoryAreas = pgTable("factory_areas", { + id: text() + .primaryKey() + .notNull() + .$default(() => generateRandomString(LENGTH)), + name: text().notNull(), + organization_id: text() + .notNull() + .references(() => organizations.id, { onDelete: "cascade" }), +}); + +export const insertFactoryAreaSchema = createInsertSchema(factoryAreas, { + id: t.String({ minLength: LENGTH }), + name: t.String({ minLength: 1 }), + organization_id: t.String({ minLength: 1 }), +}); diff --git a/packages/backend/src/db/tables/index.ts b/packages/backend/src/db/tables/index.ts new file mode 100644 index 00000000..38d521bb --- /dev/null +++ b/packages/backend/src/db/tables/index.ts @@ -0,0 +1,17 @@ +export * from "./organizations/schema"; +export * from "./systems/schema"; +export * from "./keys/schema"; +export * from "./readings/schema"; + +export * from "./systems_to_factory_areas/schema"; +export * from "./factory_areas/schema"; + +export * from "./system_models/schema"; +export * from "./parts_to_system_models/schema"; +export * from "./parts/schema"; + +export * from "./users/schema"; +export * from "./user_settings/schema"; +export * from "./sessions/schema"; +export * from "./users_to_factory_areas/schema"; +export * from "./users_to_organizations/schema"; diff --git a/packages/backend/src/db/tables/keys/queries.ts b/packages/backend/src/db/tables/keys/queries.ts new file mode 100644 index 00000000..f75b5fd3 --- /dev/null +++ b/packages/backend/src/db/tables/keys/queries.ts @@ -0,0 +1,16 @@ +import { and, eq, sql } from "drizzle-orm"; +import { keys } from ".."; +import { db } from "../../postgres"; + +const PreparedselectUnique = db + .select() + .from(keys) + .where(and(eq(keys.public_key, sql.placeholder("public_key")), eq(keys.private_key, sql.placeholder("private_key")))) + .limit(1) + .prepare("select_unique_Key"); + +export const keysQueries = { + selectUnique: async function selectUnique({ private_key, public_key }: typeof keys.$inferSelect) { + return await PreparedselectUnique.execute({ private_key, public_key }).then((v) => v.at(0)); + }, +} as const; diff --git a/packages/backend/src/db/tables/keys/schema.ts b/packages/backend/src/db/tables/keys/schema.ts new file mode 100644 index 00000000..4c120cfa --- /dev/null +++ b/packages/backend/src/db/tables/keys/schema.ts @@ -0,0 +1,24 @@ +import { pgTable, text } from "drizzle-orm/pg-core"; +import { createInsertSchema, createSelectSchema } from "drizzle-typebox"; +import { t } from "elysia"; +import { systems } from ".."; +import { generateRandomString } from "../../utils"; + +export const keys = pgTable("keys", { + public_key: text() + .primaryKey() + .$default(() => generateRandomString(22)) + .notNull(), + private_key: text() + .notNull() + .references(() => systems.id, { onDelete: "cascade" }), +}); + +keys.public_key; + +export const insertKeysSchema = createInsertSchema(keys, { + public_key: t.String({ minLength: 1 }), + private_key: t.String({ minLength: 1 }), +}); + +export const selectKeysSchema = createSelectSchema(keys); diff --git a/packages/backend/src/db/tables/organizations/schema.ts b/packages/backend/src/db/tables/organizations/schema.ts new file mode 100644 index 00000000..169c1f7a --- /dev/null +++ b/packages/backend/src/db/tables/organizations/schema.ts @@ -0,0 +1,16 @@ +import { pgTable, text } from "drizzle-orm/pg-core"; +import { createInsertSchema } from "drizzle-typebox"; +import { t } from "elysia"; +import { generateRandomString } from "../../utils"; + +export const organizations = pgTable("organizations", { + id: text() + .primaryKey() + .notNull() + .$default(() => generateRandomString(12)), + name: text().notNull(), +}); + +export const insertOrganizationsSchema = createInsertSchema(organizations, { + name: t.String({ minLength: 4 }), +}); diff --git a/packages/backend/src/db/tables/parts/schema.ts b/packages/backend/src/db/tables/parts/schema.ts new file mode 100644 index 00000000..3572488d --- /dev/null +++ b/packages/backend/src/db/tables/parts/schema.ts @@ -0,0 +1,16 @@ +import { pgTable, text } from "drizzle-orm/pg-core"; +import { createInsertSchema } from "drizzle-typebox"; +import { t } from "elysia"; +import { generateRandomString } from "../../utils"; + +export const parts = pgTable("parts", { + id: text() + .primaryKey() + .notNull() + .$default(() => generateRandomString(12)), + name: text().notNull(), +}); + +export const insertPartsSchema = createInsertSchema(parts, { + name: t.String({ minLength: 1 }), +}); diff --git a/packages/backend/src/db/tables/parts_to_system_models/schema.ts b/packages/backend/src/db/tables/parts_to_system_models/schema.ts new file mode 100644 index 00000000..c0f09c2e --- /dev/null +++ b/packages/backend/src/db/tables/parts_to_system_models/schema.ts @@ -0,0 +1,17 @@ +import { pgTable, primaryKey, text } from "drizzle-orm/pg-core"; +import { parts, systemModels } from ".."; + +export const partsToSystemModels = pgTable( + "parts_to_system_models", + { + part_id: text() + .notNull() + .references(() => parts.id, { onDelete: "cascade" }), + system_model_id: text() + .notNull() + .references(() => systemModels.id, { onDelete: "cascade" }), + }, + (table) => ({ + pk: primaryKey({ columns: [table.part_id, table.system_model_id] }), + }), +); diff --git a/packages/backend/src/db/tables/readings/api.test.ts b/packages/backend/src/db/tables/readings/api.test.ts new file mode 100644 index 00000000..79cdad6c --- /dev/null +++ b/packages/backend/src/db/tables/readings/api.test.ts @@ -0,0 +1,181 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { treaty } from "@elysiajs/eden"; +import { Elysia } from "elysia"; +import { seedDatabase } from "../../seed"; +import { readings } from "./api"; + +function createTestApi() { + const app = new Elysia().use(readings); + const api = treaty(app); + + return api; +} + +describe("Reading Post", async () => { + let seedData: Awaited>; + const api = createTestApi(); + + beforeAll(async () => { + seedData = await seedDatabase(); + }); + + it("valid credentials", async () => { + const testReadings = [ + { + time: new Date().toISOString(), + name: "temperature", + value: 25.5, + unit: "celsius", + }, + { + time: new Date().toISOString(), + name: "humidity", + value: 60, + unit: "percent", + }, + ]; + + const { status, error } = await api.reading.post(testReadings, { + headers: { + public_key: seedData.key.public_key, + private_key: seedData.key.private_key, + }, + }); + + expect(status).toBe(200); + expect(error).toBeNull(); + }); + + it("invalid credentials", async () => { + const testReadings = [ + { + time: new Date().toISOString(), + name: "temperature", + value: 25.5, + unit: "celsius", + }, + ]; + + const { status, error } = await api.reading.post(testReadings, { + headers: { + public_key: "invalid-public-key", + private_key: "invalid-private-key", + }, + }); + + expect(status).toBe(401); + expect(error?.value).toBe("The provided key does not exists"); + }); + + it("invalid data", async () => { + const invalidReading = [ + { + time: "invalid-date", // Invalid date format + name: "temperature", + value: "not-a-number", // Invalid value type + unit: "celsius", + }, + ]; + + const { status, error } = await api.reading.post( + // @ts-ignore - Intentionally testing with invalid types + invalidReading, + { + headers: { + public_key: seedData.key.public_key, + private_key: seedData.key.private_key, + }, + }, + ); + + expect(status).toBe(422); + expect(error).toBeDefined(); + }); + + it("empty data", async () => { + const { status, error } = await api.reading.post([], { + headers: { + public_key: seedData.key.public_key, + private_key: seedData.key.private_key, + }, + }); + + expect(status).toBe(422); + expect(error).toBeDefined(); + }); + + it("100 readings", async () => { + const manyReadings = Array.from({ length: 100 }, (_, i) => ({ + time: new Date().toISOString(), + name: `sensor${i}`, + value: Math.random() * 100, + unit: "units", + })); + + const { status, error } = await api.reading.post(manyReadings, { + headers: { + public_key: seedData.key.public_key, + private_key: seedData.key.private_key, + }, + }); + + expect(status).toBe(200); + expect(error).toBeNull(); + }); +}); + +describe("Readings", async () => { + let seedData: Awaited>; + const api = createTestApi(); + + beforeAll(async () => { + seedData = await seedDatabase(); + }); + + it("test", async () => { + const { readings, system } = seedData; + + const { status, error, data } = await api.readings.get({ + query: { + system_id: system.id, + startDate: readings[0].time.toISOString(), + }, + }); + + expect(status).toBe(200); + expect(error?.value).toBeUndefined(); + expect(data).toBeDefined(); + }); +}); + +describe("Latest Readings", async() => { + let seedData: Awaited>; + const api = createTestApi(); + + beforeAll(async () => { + seedData = await seedDatabase(); + }); + + it("latest reading", async () => { + + const latestReading = seedData.readings.sort((a, b) => a.time.getTime() - b.time.getTime() ).at(0) + + if (latestReading === undefined) { + return "latestReading undefined"; + } + + const { error , data, } = await api.latest_reading.get({ + query: { + name: latestReading?.name, + system_id: latestReading?.system_id + } + }) + + if (!data) { + return "latest API reading undefined"; + } + + expect(error).toBeNil(); + expect(latestReading.system_id).toBe(data.system_id) + }) +}) diff --git a/packages/backend/src/db/tables/readings/api.ts b/packages/backend/src/db/tables/readings/api.ts new file mode 100644 index 00000000..8f0dd3c9 --- /dev/null +++ b/packages/backend/src/db/tables/readings/api.ts @@ -0,0 +1,72 @@ +import Elysia, { error, t } from "elysia"; +import { Queries, Schema, Table } from "../../model"; +import { db } from "../../postgres"; +import { IsoDate } from "../../utils"; +import { AuthService } from "../../../auth/middleware"; + +export const readings = new Elysia() + .post( + "/reading", + async ({ headers, body }) => { + const key = await Queries.keys.selectUnique(headers); + if (!key) { + return error("Unauthorized", "The provided key does not exists"); + } + + const values = body.map((reading) => ({ + time: new Date(reading.time), + name: reading.name, + value: reading.value, + unit: reading.unit, + })); + + await Queries.readings.insertWithSystemId(values, key.private_key); + }, + { + headers: t.Object({ + public_key: Schema.select.keys.public_key, + private_key: Schema.select.keys.private_key, + }), + body: t.Array( + t.Object({ + time: Schema.insert.readings.time, + name: Schema.insert.readings.name, + value: Schema.insert.readings.value, + unit: Schema.insert.readings.unit, + }), + { + minItems: 1, + }, + ), + }, + ) + .get( + "/readings", + async ({ query }) => { + const readings = await Queries.readings.selectAll({ system_id: query.system_id }); + return readings; + }, + { + query: t.Object({ + system_id: Schema.insert.readings.system_id, + startDate: Schema.insert.readings.time, + endDate: t.Optional(Schema.insert.readings.time), + name: t.Optional(Schema.insert.readings.name), + limit: t.Optional(t.Number({ minimum: 1, maximum: 1000 })), + }), + }, + ) + .get( + "/latest_reading", + async ({ query: { name, system_id } }) => { + const reading = await Queries.readings.selectLatest({ system_id, name }) + + return reading; + }, + { + query: t.Object({ + system_id: Schema.insert.readings.system_id, + name: Schema.insert.readings.name + }) + } + ) diff --git a/packages/backend/src/db/tables/readings/queries.ts b/packages/backend/src/db/tables/readings/queries.ts new file mode 100644 index 00000000..307492d0 --- /dev/null +++ b/packages/backend/src/db/tables/readings/queries.ts @@ -0,0 +1,33 @@ +import { and, desc, eq, sql } from "drizzle-orm/sql"; +import { readings } from ".."; +import type { StrictOmit, StrictPick } from "../../../types/strict"; +import { db } from "../../postgres"; + +const preparedselectUnique = db + .select() + .from(readings) + .where(({ system_id }) => eq(system_id, sql.placeholder("system_id"))) + .prepare("select_readings"); + +export const readingsQueries = { + insert: async (values: (typeof readings.$inferInsert)[]) => await db.insert(readings).values(values), + insertWithSystemId: async (values: StrictOmit[], system_id: string) => { + const newValues = values.map((reading) => ({ + ...reading, + system_id, + })); + + await db.insert(readings).values(newValues); + }, + selectAll: async ({ system_id }: StrictPick) => + await preparedselectUnique.execute({ system_id }), + selectLatest: async ({ system_id, name }: StrictPick) => { + return await db + .select() + .from(readings) + .where(and(eq(readings.system_id, system_id), eq(readings.name, name))) + .orderBy(desc(readings.time)) + .limit(1) + .then(v => v.at(0)) + }, +}; diff --git a/packages/backend/src/db/tables/readings/schema.ts b/packages/backend/src/db/tables/readings/schema.ts new file mode 100644 index 00000000..837db55f --- /dev/null +++ b/packages/backend/src/db/tables/readings/schema.ts @@ -0,0 +1,30 @@ +import { pgTable, primaryKey, real, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { createInsertSchema, createSelectSchema } from "drizzle-typebox"; +import { t } from "elysia"; +import { systems } from ".."; +import { IsoDate } from "../../utils"; + +export const readings = pgTable( + "readings", + { + time: timestamp({ withTimezone: true, mode: "date" }).notNull(), + system_id: text() + .notNull() + .references(() => systems.id, { onDelete: "cascade" }), + name: text().notNull(), + value: real().notNull(), + unit: text().notNull(), + }, + (table) => ({ + pk: primaryKey({ columns: [table.time, table.system_id, table.name] }), + }), +); + +export const insertReadingsSchema = createInsertSchema(readings, { + time: t.String({ format: "iso-date-time" }), + system_id: t.String({ minLength: 1 }), + name: t.String({ minLength: 1 }), + unit: t.String({ minLength: 1 }), +}); + +export const selectReadingsSchema = createSelectSchema(readings); diff --git a/packages/backend/src/db/tables/sessions/queries.ts b/packages/backend/src/db/tables/sessions/queries.ts new file mode 100644 index 00000000..634cddba --- /dev/null +++ b/packages/backend/src/db/tables/sessions/queries.ts @@ -0,0 +1,24 @@ +import { eq, sql } from "drizzle-orm"; +import { type SessionNew, type SessionUpdate, sessions, users } from ".."; +import { db } from "../../postgres"; + +const prepareSelectUniqueWithUser = db + .select({ user: users, session: sessions }) + .from(sessions) + .innerJoin(users, eq(sessions.user_id, users.id)) + .where(eq(sessions.id, sql.placeholder("sessionId"))) + .prepare("select_with_user"); + +export const sessionQueries = { + selectWithUser: async (sessionId: string) => + await prepareSelectUniqueWithUser.execute({ sessionId }).then((v) => v.at(0)), + create: async (session: SessionNew) => { + await db.insert(sessions).values(session); + }, + delete: async (sessionId: string) => { + await db.delete(sessions).where(eq(sessions.id, sessionId)); + }, + update: async (session: SessionUpdate) => { + await db.update(sessions).set(session).where(eq(sessions.id, session.id)); + }, +} as const; diff --git a/packages/backend/src/db/tables/sessions/schema.ts b/packages/backend/src/db/tables/sessions/schema.ts new file mode 100644 index 00000000..07822a80 --- /dev/null +++ b/packages/backend/src/db/tables/sessions/schema.ts @@ -0,0 +1,23 @@ +import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { createInsertSchema } from "drizzle-typebox"; +import { t } from "elysia"; +import type { Prettify } from "elysia/types"; +import { users } from ".."; +import type { StrictOmit, StrictPick } from "../../../types/strict"; + +export const sessions = pgTable("sessions", { + id: text().primaryKey().notNull(), + user_id: text() + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + expires_at: timestamp({ mode: "date" }).notNull(), +}); + +export const insertSessionsSchema = createInsertSchema(sessions, { + user_id: t.String({ minLength: 1 }), + expires_at: t.String({ format: "date" }), +}); + +export type Session = typeof sessions.$inferSelect; +export type SessionNew = typeof sessions.$inferSelect; +export type SessionUpdate = Prettify & Partial>>; diff --git a/packages/backend/src/db/tables/system_models/schema.ts b/packages/backend/src/db/tables/system_models/schema.ts new file mode 100644 index 00000000..feb4a9ab --- /dev/null +++ b/packages/backend/src/db/tables/system_models/schema.ts @@ -0,0 +1,17 @@ +import { pgTable, text, uuid } from "drizzle-orm/pg-core"; +import { createInsertSchema, createSelectSchema } from "drizzle-typebox"; +import { t } from "elysia"; +import { generateRandomString } from "../../utils"; + +export const systemModels = pgTable("system_models", { + id: text() + .primaryKey() + .notNull() + .$default(() => generateRandomString(12)), + name: text().notNull(), +}); + +export const insertSystemModelsSchema = createInsertSchema(systemModels, { + id: t.String({ minLength: 12 }), + name: t.String({ minLength: 1 }), +}); diff --git a/packages/backend/src/db/tables/systems/schema.ts b/packages/backend/src/db/tables/systems/schema.ts new file mode 100644 index 00000000..510d86c9 --- /dev/null +++ b/packages/backend/src/db/tables/systems/schema.ts @@ -0,0 +1,25 @@ +import { pgTable, text } from "drizzle-orm/pg-core"; +import { createInsertSchema } from "drizzle-typebox"; +import { t } from "elysia"; +import { systemModels } from ".."; +import { generateRandomString } from "../../utils"; +import { organizations } from "../organizations/schema"; + +export const systems = pgTable("systems", { + id: text() + .primaryKey() + .notNull() + .$default(() => generateRandomString(12)), + name: text().notNull(), + organization_id: text() + .notNull() + .references(() => organizations.id, { onDelete: "cascade" }), + system_model_id: text().references(() => systemModels.id, { onDelete: "set null" }), +}); + +export const insertSystemsSchema = createInsertSchema(systems, { + id: t.String({ minLength: 12 }), + name: t.String({ minLength: 1 }), + organization_id: t.String({ minLength: 1 }), + system_model_id: t.String({ minLength: 1 }), +}); diff --git a/packages/backend/src/db/tables/systems_to_factory_areas/schema.ts b/packages/backend/src/db/tables/systems_to_factory_areas/schema.ts new file mode 100644 index 00000000..918f7f5b --- /dev/null +++ b/packages/backend/src/db/tables/systems_to_factory_areas/schema.ts @@ -0,0 +1,18 @@ +import { pgTable, primaryKey, text, uuid } from "drizzle-orm/pg-core"; +import { factoryAreas } from "../factory_areas/schema"; +import { systems } from "../systems/schema"; + +export const systemsToFactoryAreas = pgTable( + "systems_to_factory_areas", + { + system_id: text() + .notNull() + .references(() => systems.id, { onDelete: "cascade" }), + factory_area_id: text() + .notNull() + .references(() => factoryAreas.id, { onDelete: "cascade" }), + }, + (table) => ({ + pk: primaryKey({ columns: [table.system_id, table.factory_area_id] }), + }), +); diff --git a/packages/backend/src/db/tables/user_settings/schema.ts b/packages/backend/src/db/tables/user_settings/schema.ts new file mode 100644 index 00000000..dc4a8593 --- /dev/null +++ b/packages/backend/src/db/tables/user_settings/schema.ts @@ -0,0 +1,23 @@ +import { boolean, pgTable, text, uuid } from "drizzle-orm/pg-core"; +import { createInsertSchema } from "drizzle-typebox"; +import { t } from "elysia"; +import { users } from ".."; +import { generateRandomString } from "../../utils"; + +export const userSettings = pgTable("user_settings", { + id: text() + .primaryKey() + .notNull() + .$default(() => generateRandomString(12)), + theme: text().notNull(), + product_updates: boolean().notNull(), + user_id: text() + .notNull() + .references(() => users.id, { onDelete: "cascade" }), +}); + +export const insertUserSettingsSchema = createInsertSchema(userSettings, { + id: t.String({ minLength: 12 }), + theme: t.String({ minLength: 1 }), + user_id: t.String({ minLength: 12 }), +}); diff --git a/packages/backend/src/db/tables/users/queries.ts b/packages/backend/src/db/tables/users/queries.ts new file mode 100644 index 00000000..45df2068 --- /dev/null +++ b/packages/backend/src/db/tables/users/queries.ts @@ -0,0 +1,21 @@ +import { and, eq } from "drizzle-orm"; +import type { User, UserNew } from ".."; +import type { StrictPick } from "../../../types/strict"; +import { Table } from "../../model"; +import { db } from "../../postgres"; + +export const usersQueries = { + selectUniqueWithProvider: async (user: StrictPick) => + await db + .select() + .from(Table.users) + .where(and(eq(Table.users.provider_name, user.provider_name), eq(Table.users.provider_id, user.provider_id))) + .then((v) => v.at(0)), + create: async (user: UserNew) => { + return await db + .insert(Table.users) + .values(user) + .returning() + .then((v) => v[0]); + }, +}; diff --git a/packages/backend/src/db/tables/users/schema.ts b/packages/backend/src/db/tables/users/schema.ts new file mode 100644 index 00000000..aebdce24 --- /dev/null +++ b/packages/backend/src/db/tables/users/schema.ts @@ -0,0 +1,23 @@ +import { boolean, pgEnum, pgTable, text, uuid } from "drizzle-orm/pg-core"; +import { createInsertSchema } from "drizzle-typebox"; +import { t } from "elysia"; +import { generateRandomString } from "../../utils"; + +export const providerEnum = pgEnum("providers", ["Github", "Microsoft"]); + +export const users = pgTable("users", { + id: text() + .primaryKey() + .notNull() + .$default(() => generateRandomString(12)), + is_superadmin: boolean().notNull().default(false), + provider_name: providerEnum().notNull(), + provider_id: text().notNull(), +}); + +export const insertUserSchema = createInsertSchema(users, { + id: t.String({ minLength: 12 }), +}); + +export type User = typeof users.$inferSelect; +export type UserNew = typeof users.$inferInsert; diff --git a/packages/backend/src/db/tables/users_to_factory_areas/schema.ts b/packages/backend/src/db/tables/users_to_factory_areas/schema.ts new file mode 100644 index 00000000..6b4e1b65 --- /dev/null +++ b/packages/backend/src/db/tables/users_to_factory_areas/schema.ts @@ -0,0 +1,17 @@ +import { pgTable, primaryKey, text } from "drizzle-orm/pg-core"; +import { factoryAreas, users } from ".."; + +export const usersToFactoryAreas = pgTable( + "users_to_factory_areas", + { + user_id: text() + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + factory_area_id: text() + .notNull() + .references(() => factoryAreas.id, { onDelete: "cascade" }), + }, + (table) => ({ + pk: primaryKey({ columns: [table.user_id, table.factory_area_id] }), + }), +); diff --git a/packages/backend/src/db/tables/users_to_organizations/schema.ts b/packages/backend/src/db/tables/users_to_organizations/schema.ts new file mode 100644 index 00000000..3403281c --- /dev/null +++ b/packages/backend/src/db/tables/users_to_organizations/schema.ts @@ -0,0 +1,18 @@ +import { pgTable, primaryKey, text } from "drizzle-orm/pg-core"; +import { organizations, users } from ".."; + +export const usersToOrganizations = pgTable( + "users_to_organizations", + { + organization_id: text() + .notNull() + .references(() => organizations.id, { onDelete: "cascade" }), + user_id: text() + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + role: text().notNull(), + }, + (table) => ({ + pk: primaryKey({ columns: [table.organization_id, table.user_id] }), + }), +); diff --git a/packages/backend/src/db/timescale.ts b/packages/backend/src/db/timescale.ts new file mode 100644 index 00000000..6892f6ae --- /dev/null +++ b/packages/backend/src/db/timescale.ts @@ -0,0 +1,32 @@ +import { type SQL, type SQLWrapper, sql } from "drizzle-orm"; + +export const validUnits = [ + "microseconds", + "milliseconds", + "second", + "seconds", + "minute", + "minutes", + "hour", + "hours", + "day", + "days", + "week", + "weeks", + "month", + "months", + "year", + "years", +] as const; + +export type ValidUnits = (typeof validUnits)[number]; + +export type IntervalString = `${number} ${ValidUnits}`; + +// Define the time_bucket function with type-safe interval argument +export function time_bucket( + expression: T, + interval: IntervalString, +): SQL { + return sql`time_bucket(${interval}, ${expression})`.mapWith((v) => new Date(v)); +} diff --git a/packages/backend/src/db/utils.ts b/packages/backend/src/db/utils.ts new file mode 100644 index 00000000..4459fa44 --- /dev/null +++ b/packages/backend/src/db/utils.ts @@ -0,0 +1,97 @@ +/** + * @lastModified 2024-10-10 + * @see https://elysiajs.com/recipe/drizzle.html#utility + */ +import { Kind, type TObject } from "@sinclair/typebox"; +import { + type BuildInsertSchema, + type BuildSelectSchema, + createInsertSchema, + createSelectSchema, +} from "drizzle-typebox"; + +import type { Table } from "drizzle-orm"; +import { customAlphabet } from "nanoid"; +import { error, t } from "elysia"; + + +type Spread = T extends TObject + ? { + [K in keyof Fields]: Fields[K]; + } + : T extends Table + ? Mode extends "select" + ? BuildSelectSchema + : Mode extends "insert" + ? BuildInsertSchema + : {} + : {}; + +/** + * Spread a Drizzle schema into a plain object + */ +export const spread = ( + schema: T, + mode?: Mode, +): Spread => { + const newSchema: Record = {}; + let table; + + switch (mode) { + case "insert": + case "select": + if (Kind in schema) { + table = schema; + break; + } + + table = mode === "insert" ? createInsertSchema(schema) : createSelectSchema(schema); + + break; + + default: + if (!(Kind in schema)) throw new Error("Expect a schema"); + table = schema; + } + + for (const key of Object.keys(table.properties)) newSchema[key] = table.properties[key]; + + return newSchema as any; +}; + +/** +* Spread a Drizzle Table into a plain object +* +* If `mode` is 'insert', the schema will be refined for insert +* If `mode` is 'select', the schema will be refined for select +* If `mode` is undefined, the schema will be spread as is, models will need to be refined manually +* @example +``` +const schema = spread(table, 'insert') +const schema = spread(table, 'select') +``` +*/ +export const spreads = , Mode extends "select" | "insert" | undefined>( + models: T, + mode?: Mode, +): { + [K in keyof T]: Spread; +} => { + const newSchema: Record = {}; + const keys = Object.keys(models); + + for (const key of keys) newSchema[key] = spread(models[key], mode); + + return newSchema as any; +}; + + + +// Used for generating random ids for primary keys +export const generateRandomString = customAlphabet("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"); + + +export const IsoDate = t.Transform(t.String({format: "iso-date-time"})) + .Decode((v) => new Date(v)) + .Encode((v) => v.toISOString()); + diff --git a/packages/backend/src/environment.ts b/packages/backend/src/environment.ts new file mode 100644 index 00000000..c34782f9 --- /dev/null +++ b/packages/backend/src/environment.ts @@ -0,0 +1,45 @@ +import { exit } from "node:process"; +import { Value } from "@sinclair/typebox/value"; +import { env } from "bun"; +import { t } from "elysia"; + +const optionalEnviromentSchema = t.Object({ + GITHUB_CLIENT_ID: t.String({ minLength: 1 }), + GITHUB_CLIENT_SECRET: t.String({ minLength: 1 }), + MICROSOFT_TENANT_ID: t.String({ minLength: 1 }), + MICROSOFT_CLIENT_ID: t.String({ minLength: 1 }), + MICROSOFT_CLIENT_SECRET: t.String({ minLength: 1 }), + MICROSOFT_REDIRECT_URI: t.String({ minLength: 1 }), +}); + +const requiredEnviromentSchema = t.Object({ + DATABASE_URL: t.String({ minLength: 1 }), + PROD: t.Boolean(), +}); + +const enviromentSchema = t.Intersect([ + requiredEnviromentSchema, + optionalEnviromentSchema +]); + +let cleanedEnv: unknown; +cleanedEnv = Value.Convert(enviromentSchema, env); +cleanedEnv = Value.Clean(enviromentSchema, cleanedEnv); + +function parseEnviroment(schema: typeof requiredEnviromentSchema | typeof enviromentSchema) { + + if (Value.Check(schema, cleanedEnv) === false) { + console.error("Errors while compiling config"); + const errors = Value.Errors(schema, env); + for (const error of errors) { + console.log(`Failed to parse ${"\x1b[33m"}${error.path.slice(1)}${"\x1b[0m"}: ${error.message}`); + } + exit(1); + } + + return Value.Encode(schema, cleanedEnv) +} + +const input = env.PROD === "true" ? enviromentSchema : requiredEnviromentSchema + +export const environment = parseEnviroment(input) as typeof enviromentSchema.static diff --git a/packages/backend/src/types/errors.ts b/packages/backend/src/types/errors.ts new file mode 100644 index 00000000..8de5350d --- /dev/null +++ b/packages/backend/src/types/errors.ts @@ -0,0 +1,18 @@ +export function catchError Error, Errors extends E[] = E[]>( + promise: Promise, + errorToCatch?: [...Errors], +): Promise<[undefined, T] | [InstanceType]> { + return promise + .then((data) => { + return [undefined, data] as [undefined, T]; + }) + .catch((error) => { + if (errorToCatch === undefined) { + return [error]; + } + if (errorToCatch.some((e) => error instanceof e)) { + return [error] as [InstanceType]; + } + throw error; + }); +} diff --git a/packages/backend/src/types/strict.ts b/packages/backend/src/types/strict.ts new file mode 100644 index 00000000..0d3f4005 --- /dev/null +++ b/packages/backend/src/types/strict.ts @@ -0,0 +1,27 @@ +/** + * A utility type to improve readability of complex types by removing excess type information + * @template T The type to prettify + */ +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + +/** + * A type-safe version of Omit that allows omitting either a single key or multiple keys from a type. + * @example + * + * // Omit a single key + * type UserWithoutId = StrictOmit; + * // Equivalent to: { name: string; email: string; age: number; } + * + * // Omit multiple keys + * type ReducedUser = StrictOmit; + * // Equivalent to: { name: string; email: string; } + */ +export type StrictOmit = Prettify< + Omit +>; + +export type StrictPick = Prettify< + Pick +>; From 68fe5905d374fcec6bffbb0353637a01d3f8142f Mon Sep 17 00:00:00 2001 From: Hans Askov Date: Mon, 25 Nov 2024 23:34:54 +0100 Subject: [PATCH 003/341] add package.json updates --- .gitignore | 3 --- bun.lockb | Bin 0 -> 133068 bytes package.json | 16 ++++++++++++ packages/backend/package.json | 44 +++++++++++++++++++++++++++++++++ packages/frontend/package.json | 37 +++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 bun.lockb create mode 100644 package.json create mode 100644 packages/backend/package.json create mode 100644 packages/frontend/package.json diff --git a/.gitignore b/.gitignore index 97417b85..c05085a5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,9 +9,6 @@ *backend.exe -# Logs -package.json -bun.lockb logs _.log diff --git a/bun.lockb b/bun.lockb new file mode 100644 index 0000000000000000000000000000000000000000..fe0aaee74eff2d76e1cab6cc1d0edc0428ea4ec6 GIT binary patch literal 133068 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