CMU Coding Bootcamp

feat: backend unit / SQL & NoSQL

thecoded.prof 8cdf1444 be13960b

verified
+747 -93
+1
.gitignore
··· 1 1 node_modules 2 + .devenv
+315
backend/.devenv.flake.nix
··· 1 + { 2 + inputs = 3 + let 4 + version = "1.9.0"; 5 + system = "x86_64-linux"; 6 + devenv_root = "/home/coded/Programming/CMU/backend"; 7 + devenv_dotfile = "/home/coded/Programming/CMU/backend/.devenv"; 8 + devenv_dotfile_path = ./.devenv; 9 + devenv_tmpdir = "/run/user/1000"; 10 + devenv_runtime = "/run/user/1000/devenv-684c98d"; 11 + devenv_istesting = false; 12 + devenv_direnvrc_latest_version = 1; 13 + container_name = null; 14 + active_profiles = [ ]; 15 + hostname = "shorthair"; 16 + username = "coded"; 17 + 18 + in { 19 + git-hooks.url = "github:cachix/git-hooks.nix"; 20 + git-hooks.inputs.nixpkgs.follows = "nixpkgs"; 21 + pre-commit-hooks.follows = "git-hooks"; 22 + nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling"; 23 + devenv.url = "github:cachix/devenv?dir=src/modules"; 24 + } // (if builtins.pathExists (devenv_dotfile_path + "/flake.json") 25 + then builtins.fromJSON (builtins.readFile (devenv_dotfile_path + "/flake.json")) 26 + else { }); 27 + 28 + outputs = { nixpkgs, ... }@inputs: 29 + let 30 + version = "1.9.0"; 31 + system = "x86_64-linux"; 32 + devenv_root = "/home/coded/Programming/CMU/backend"; 33 + devenv_dotfile = "/home/coded/Programming/CMU/backend/.devenv"; 34 + devenv_dotfile_path = ./.devenv; 35 + devenv_tmpdir = "/run/user/1000"; 36 + devenv_runtime = "/run/user/1000/devenv-684c98d"; 37 + devenv_istesting = false; 38 + devenv_direnvrc_latest_version = 1; 39 + container_name = null; 40 + active_profiles = [ ]; 41 + hostname = "shorthair"; 42 + username = "coded"; 43 + 44 + devenv = 45 + if builtins.pathExists (devenv_dotfile_path + "/devenv.json") 46 + then builtins.fromJSON (builtins.readFile (devenv_dotfile_path + "/devenv.json")) 47 + else { }; 48 + 49 + systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 50 + 51 + # Function to create devenv configuration for a specific system with profiles support 52 + mkDevenvForSystem = targetSystem: 53 + let 54 + getOverlays = inputName: inputAttrs: 55 + map 56 + (overlay: 57 + let 58 + input = inputs.${inputName} or (throw "No such input `${inputName}` while trying to configure overlays."); 59 + in 60 + input.overlays.${overlay} or (throw "Input `${inputName}` has no overlay called `${overlay}`. Supported overlays: ${nixpkgs.lib.concatStringsSep ", " (builtins.attrNames input.overlays)}")) 61 + inputAttrs.overlays or [ ]; 62 + overlays = nixpkgs.lib.flatten (nixpkgs.lib.mapAttrsToList getOverlays (devenv.inputs or { })); 63 + permittedUnfreePackages = devenv.nixpkgs.per-platform."${targetSystem}".permittedUnfreePackages or devenv.nixpkgs.permittedUnfreePackages or [ ]; 64 + pkgs = import nixpkgs { 65 + system = targetSystem; 66 + config = { 67 + allowUnfree = devenv.nixpkgs.per-platform."${targetSystem}".allowUnfree or devenv.nixpkgs.allowUnfree or devenv.allowUnfree or false; 68 + allowBroken = devenv.nixpkgs.per-platform."${targetSystem}".allowBroken or devenv.nixpkgs.allowBroken or devenv.allowBroken or false; 69 + cudaSupport = devenv.nixpkgs.per-platform."${targetSystem}".cudaSupport or devenv.nixpkgs.cudaSupport or false; 70 + cudaCapabilities = devenv.nixpkgs.per-platform."${targetSystem}".cudaCapabilities or devenv.nixpkgs.cudaCapabilities or [ ]; 71 + permittedInsecurePackages = devenv.nixpkgs.per-platform."${targetSystem}".permittedInsecurePackages or devenv.nixpkgs.permittedInsecurePackages or devenv.permittedInsecurePackages or [ ]; 72 + allowUnfreePredicate = if (permittedUnfreePackages != [ ]) then (pkg: builtins.elem (nixpkgs.lib.getName pkg) permittedUnfreePackages) else (_: false); 73 + }; 74 + inherit overlays; 75 + }; 76 + lib = pkgs.lib; 77 + importModule = path: 78 + if lib.hasPrefix "./" path 79 + then if lib.hasSuffix ".nix" path 80 + then ./. + (builtins.substring 1 255 path) 81 + else ./. + (builtins.substring 1 255 path) + "/devenv.nix" 82 + else if lib.hasPrefix "../" path 83 + then throw "devenv: ../ is not supported for imports" 84 + else 85 + let 86 + paths = lib.splitString "/" path; 87 + name = builtins.head paths; 88 + input = inputs.${name} or (throw "Unknown input ${name}"); 89 + subpath = "/${lib.concatStringsSep "/" (builtins.tail paths)}"; 90 + devenvpath = "${input}" + subpath; 91 + devenvdefaultpath = devenvpath + "/devenv.nix"; 92 + in 93 + if lib.hasSuffix ".nix" devenvpath 94 + then devenvpath 95 + else if builtins.pathExists devenvdefaultpath 96 + then devenvdefaultpath 97 + else throw (devenvdefaultpath + " file does not exist for input ${name}."); 98 + 99 + # Phase 1: Base evaluation to extract profile definitions 100 + baseProject = pkgs.lib.evalModules { 101 + specialArgs = inputs // { inherit inputs; }; 102 + modules = [ 103 + ({ config, ... }: { 104 + _module.args.pkgs = pkgs.appendOverlays (config.overlays or [ ]); 105 + }) 106 + (inputs.devenv.modules + /top-level.nix) 107 + { 108 + devenv.cliVersion = version; 109 + devenv.root = devenv_root; 110 + devenv.dotfile = devenv_dotfile; 111 + } 112 + ({ options, ... }: { 113 + config.devenv = lib.mkMerge [ 114 + (pkgs.lib.optionalAttrs (builtins.hasAttr "tmpdir" options.devenv) { 115 + tmpdir = devenv_tmpdir; 116 + }) 117 + (pkgs.lib.optionalAttrs (builtins.hasAttr "isTesting" options.devenv) { 118 + isTesting = devenv_istesting; 119 + }) 120 + (pkgs.lib.optionalAttrs (builtins.hasAttr "runtime" options.devenv) { 121 + runtime = devenv_runtime; 122 + }) 123 + (pkgs.lib.optionalAttrs (builtins.hasAttr "direnvrcLatestVersion" options.devenv) { 124 + direnvrcLatestVersion = devenv_direnvrc_latest_version; 125 + }) 126 + ]; 127 + }) 128 + (pkgs.lib.optionalAttrs (container_name != null) { 129 + container.isBuilding = pkgs.lib.mkForce true; 130 + containers.${container_name}.isBuilding = true; 131 + }) 132 + ] ++ (map importModule (devenv.imports or [ ])) ++ [ 133 + (if builtins.pathExists ./devenv.nix then ./devenv.nix else { }) 134 + (devenv.devenv or { }) 135 + (if builtins.pathExists ./devenv.local.nix then ./devenv.local.nix else { }) 136 + (if builtins.pathExists (devenv_dotfile_path + "/cli-options.nix") then import (devenv_dotfile_path + "/cli-options.nix") else { }) 137 + ]; 138 + }; 139 + 140 + # Phase 2: Extract and apply profiles using extendModules with priority overrides 141 + project = 142 + let 143 + # Build ordered list of profile names: hostname -> user -> manual 144 + manualProfiles = active_profiles; 145 + currentHostname = hostname; 146 + currentUsername = username; 147 + hostnameProfiles = lib.optional (currentHostname != "" && builtins.hasAttr currentHostname (baseProject.config.profiles.hostname or { })) "hostname.${currentHostname}"; 148 + userProfiles = lib.optional (currentUsername != "" && builtins.hasAttr currentUsername (baseProject.config.profiles.user or { })) "user.${currentUsername}"; 149 + 150 + # Ordered list of profiles to activate 151 + orderedProfiles = hostnameProfiles ++ userProfiles ++ manualProfiles; 152 + 153 + # Resolve profile extends with cycle detection 154 + resolveProfileExtends = profileName: visited: 155 + if builtins.elem profileName visited then 156 + throw "Circular dependency detected in profile extends: ${lib.concatStringsSep " -> " visited} -> ${profileName}" 157 + else 158 + let 159 + profile = getProfileConfig profileName; 160 + extends = profile.extends or [ ]; 161 + newVisited = visited ++ [ profileName ]; 162 + extendedProfiles = lib.flatten (map (name: resolveProfileExtends name newVisited) extends); 163 + in 164 + extendedProfiles ++ [ profileName ]; 165 + 166 + # Get profile configuration by name from baseProject 167 + getProfileConfig = profileName: 168 + if lib.hasPrefix "hostname." profileName then 169 + let name = lib.removePrefix "hostname." profileName; 170 + in baseProject.config.profiles.hostname.${name} 171 + else if lib.hasPrefix "user." profileName then 172 + let name = lib.removePrefix "user." profileName; 173 + in baseProject.config.profiles.user.${name} 174 + else 175 + let 176 + availableProfiles = builtins.attrNames (baseProject.config.profiles or { }); 177 + hostnameProfiles = map (n: "hostname.${n}") (builtins.attrNames (baseProject.config.profiles.hostname or { })); 178 + userProfiles = map (n: "user.${n}") (builtins.attrNames (baseProject.config.profiles.user or { })); 179 + allAvailableProfiles = availableProfiles ++ hostnameProfiles ++ userProfiles; 180 + in 181 + baseProject.config.profiles.${profileName} or (throw "Profile '${profileName}' not found. Available profiles: ${lib.concatStringsSep ", " allAvailableProfiles}"); 182 + 183 + # Fold over ordered profiles to build final list with extends 184 + expandedProfiles = lib.foldl' 185 + (acc: profileName: 186 + let 187 + allProfileNames = resolveProfileExtends profileName [ ]; 188 + in 189 + acc ++ allProfileNames 190 + ) [ ] 191 + orderedProfiles; 192 + 193 + # Map over expanded profiles and apply priorities 194 + allPrioritizedModules = lib.imap0 195 + (index: profileName: 196 + let 197 + # Decrement priority for each profile (lower = higher precedence) 198 + # Start with the next lowest priority after the default priority for values (100) 199 + profilePriority = (lib.modules.defaultOverridePriority - 1) - index; 200 + profileConfig = getProfileConfig profileName; 201 + 202 + # Support overriding both plain attrset modules and functions 203 + applyModuleOverride = config: 204 + if builtins.isFunction config 205 + then 206 + let 207 + wrapper = args: applyOverrideRecursive (config args); 208 + in 209 + lib.mirrorFunctionArgs config wrapper 210 + else applyOverrideRecursive config; 211 + 212 + # Apply overrides recursively 213 + applyOverrideRecursive = config: 214 + if lib.isAttrs config && config ? _type 215 + then config # Don't override values with existing type metadata 216 + else if lib.isAttrs config 217 + then lib.mapAttrs (_: applyOverrideRecursive) config 218 + else lib.mkOverride profilePriority config; 219 + 220 + # Apply priority overrides recursively to the deferredModule imports structure 221 + prioritizedConfig = ( 222 + profileConfig.module // { 223 + imports = lib.map 224 + (importItem: 225 + importItem // { 226 + imports = lib.map 227 + (nestedImport: 228 + applyModuleOverride nestedImport 229 + ) 230 + (importItem.imports or [ ]); 231 + } 232 + ) 233 + (profileConfig.module.imports or [ ]); 234 + } 235 + ); 236 + in 237 + prioritizedConfig 238 + ) 239 + expandedProfiles; 240 + in 241 + if allPrioritizedModules == [ ] 242 + then baseProject 243 + else baseProject.extendModules { modules = allPrioritizedModules; }; 244 + 245 + config = project.config; 246 + 247 + options = pkgs.nixosOptionsDoc { 248 + options = builtins.removeAttrs project.options [ "_module" ]; 249 + warningsAreErrors = false; 250 + # Unpack Nix types, e.g. literalExpression, mDoc. 251 + transformOptions = 252 + let isDocType = v: builtins.elem v [ "literalDocBook" "literalExpression" "literalMD" "mdDoc" ]; 253 + in lib.attrsets.mapAttrs (_: v: 254 + if v ? _type && isDocType v._type then 255 + v.text 256 + else if v ? _type && v._type == "derivation" then 257 + v.name 258 + else 259 + v 260 + ); 261 + }; 262 + 263 + # Recursively search for outputs in the config. 264 + # This is used when not building a specific output by attrpath. 265 + build = options: config: 266 + lib.concatMapAttrs 267 + (name: option: 268 + if lib.isOption option then 269 + let typeName = option.type.name or ""; 270 + in 271 + if builtins.elem typeName [ "output" "outputOf" ] then 272 + { ${name} = config.${name}; } 273 + else { } 274 + else if builtins.isAttrs option && !lib.isDerivation option then 275 + let v = build option config.${name}; 276 + in if v != { } then { 277 + ${name} = v; 278 + } else { } 279 + else { } 280 + ) 281 + options; 282 + in 283 + { 284 + inherit config options build; 285 + shell = config.shell; 286 + packages = { 287 + optionsJSON = options.optionsJSON; 288 + # deprecated 289 + inherit (config) info procfileScript procfileEnv procfile; 290 + ci = config.ciDerivation; 291 + }; 292 + }; 293 + 294 + # Generate per-system devenv configurations 295 + perSystem = nixpkgs.lib.genAttrs systems mkDevenvForSystem; 296 + 297 + # Default devenv for the current system 298 + currentSystemDevenv = perSystem.${system}; 299 + in 300 + { 301 + devShell = nixpkgs.lib.genAttrs systems (s: perSystem.${s}.shell); 302 + packages = nixpkgs.lib.genAttrs systems (s: perSystem.${s}.packages); 303 + 304 + # Per-system devenv configurations 305 + devenv = { 306 + # Default devenv for the current system 307 + inherit (currentSystemDevenv) config options build shell packages; 308 + # Per-system devenv configurations 309 + inherit perSystem; 310 + }; 311 + 312 + # Legacy build output 313 + build = currentSystemDevenv.build currentSystemDevenv.options currentSystemDevenv.config; 314 + }; 315 + }
+116
backend/dec15.sql
··· 1 + -- CREATE 2 + INSERT INTO 3 + SUPPLY_CHAIN.SUPPLIER 4 + VALUES 5 + ( 6 + 'amaz', 7 + 'amazon', 8 + '410 Terry Ave N', 9 + 'Seattle', 10 + 0, 11 + 'SILVER' 12 + ), 13 + ( 14 + 'goog', 15 + 'google', 16 + '1600 Amphitheatre Parkway', 17 + 'Mountain View', 18 + 0, 19 + 'GOLD' 20 + ); 21 + 22 + INSERT INTO 23 + SUPPLY_CHAIN.PRODUCT 24 + VALUES 25 + ('whwine', 'white wine', 'white', 100, ''), 26 + ('rewine', 'red wine', 'red', 150, ''), 27 + ('rowine', 'rose wine', 'rose', 0, ''), 28 + ('prosec', 'prosecco', 'sparkling', 500, ''), 29 + ('clubso', 'club soda', 'sparkling', 200, ''); 30 + 31 + INSERT INTO 32 + SUPPLY_CHAIN.SUPPLIES 33 + VALUES 34 + ('amaz', 'whwine', 10.22, '17:00'), 35 + ('goog', 'rowine', 5.16, '23:00'), 36 + ('amaz', 'rewine', 7.12, '19:00'), 37 + ('goog', 'clubso', 3.07, '03:00'), 38 + ('amaz', 'prosec', 9.99, '05:00'); 39 + 40 + INSERT INTO 41 + SUPPLY_CHAIN.PURCHASE_ORDER 42 + VALUES 43 + ('aaaaaaa', '2005-01-01', 'amaz'), 44 + ('bbbbbbb', '2017-05-24', 'goog'), 45 + ('ccccccc', '2013-09-30', 'amaz'), 46 + ('ddddddd', '2020-12-01', 'goog'), 47 + ('eeeeeee', '2025-12-16', 'amaz'); 48 + 49 + INSERT INTO 50 + SUPPLY_CHAIN.PO_LINE 51 + VALUES 52 + ('aaaaaaa', 'whwine', 5), 53 + ('bbbbbbb', 'rowine', 50), 54 + ('ccccccc', 'rewine', 10), 55 + ('ddddddd', 'clubso', 150), 56 + ('eeeeeee', 'prosec', 200); 57 + 58 + -- READ 59 + SELECT 60 + * 61 + FROM 62 + SUPPLY_CHAIN.SUPPLIER; 63 + 64 + SELECT 65 + * 66 + FROM 67 + SUPPLY_CHAIN.PRODUCT; 68 + 69 + SELECT 70 + * 71 + FROM 72 + SUPPLY_CHAIN.SUPPLIES; 73 + 74 + SELECT 75 + * 76 + FROM 77 + SUPPLY_CHAIN.PURCHASE_ORDER; 78 + 79 + SELECT 80 + P.PRODNR, 81 + P.PRODNAME, 82 + P.PRODTYPE, 83 + P.AVAILABLE_QUANTITY, 84 + PL.PONR, 85 + PL.QUANTITY 86 + FROM 87 + SUPPLY_CHAIN.PRODUCT P 88 + RIGHT JOIN SUPPLY_CHAIN.PO_LINE PL ON P.PRODNR = PL.PRODNR 89 + WHERE 90 + P.AVAILABLE_QUANTITY > 10; 91 + 92 + -- UPDATE 93 + UPDATE SUPPLY_CHAIN.supplier 94 + SET 95 + SUPSTATUS = 100 96 + WHERE 97 + SUPNR = 'amaz'; 98 + 99 + UPDATE SUPPLY_CHAIN.PRODUCT 100 + SET 101 + "available_quantity" = 10 102 + WHERE 103 + AVAILABLE_QUANTITY < 10; 104 + 105 + -- DELETE 106 + DELETE FROM SUPPLY_CHAIN.PURCHASE_ORDER WHERE PONR = 'aaaaaaa'; 107 + 108 + DELETE FROM SUPPLY_CHAIN.SUPPLIER; 109 + 110 + DELETE FROM SUPPLY_CHAIN.PRODUCT; 111 + 112 + DELETE FROM SUPPLY_CHAIN.PO_LINE; 113 + 114 + DELETE FROM SUPPLY_CHAIN.PURCHASE_ORDER; 115 + 116 + DELETE FROM SUPPLY_CHAIN.SUPPLIES;
+103
backend/devenv.lock
··· 1 + { 2 + "nodes": { 3 + "devenv": { 4 + "locked": { 5 + "dir": "src/modules", 6 + "lastModified": 1765898645, 7 + "owner": "cachix", 8 + "repo": "devenv", 9 + "rev": "b1ca338a97209fa6cdd18dede3f82e284cbcfb0d", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "dir": "src/modules", 14 + "owner": "cachix", 15 + "repo": "devenv", 16 + "type": "github" 17 + } 18 + }, 19 + "flake-compat": { 20 + "flake": false, 21 + "locked": { 22 + "lastModified": 1765121682, 23 + "owner": "edolstra", 24 + "repo": "flake-compat", 25 + "rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3", 26 + "type": "github" 27 + }, 28 + "original": { 29 + "owner": "edolstra", 30 + "repo": "flake-compat", 31 + "type": "github" 32 + } 33 + }, 34 + "git-hooks": { 35 + "inputs": { 36 + "flake-compat": "flake-compat", 37 + "gitignore": "gitignore", 38 + "nixpkgs": [ 39 + "nixpkgs" 40 + ] 41 + }, 42 + "locked": { 43 + "lastModified": 1765464257, 44 + "owner": "cachix", 45 + "repo": "git-hooks.nix", 46 + "rev": "09e45f2598e1a8499c3594fe11ec2943f34fe509", 47 + "type": "github" 48 + }, 49 + "original": { 50 + "owner": "cachix", 51 + "repo": "git-hooks.nix", 52 + "type": "github" 53 + } 54 + }, 55 + "gitignore": { 56 + "inputs": { 57 + "nixpkgs": [ 58 + "git-hooks", 59 + "nixpkgs" 60 + ] 61 + }, 62 + "locked": { 63 + "lastModified": 1762808025, 64 + "owner": "hercules-ci", 65 + "repo": "gitignore.nix", 66 + "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", 67 + "type": "github" 68 + }, 69 + "original": { 70 + "owner": "hercules-ci", 71 + "repo": "gitignore.nix", 72 + "type": "github" 73 + } 74 + }, 75 + "nixpkgs": { 76 + "locked": { 77 + "lastModified": 1764580874, 78 + "owner": "cachix", 79 + "repo": "devenv-nixpkgs", 80 + "rev": "dcf61356c3ab25f1362b4a4428a6d871e84f1d1d", 81 + "type": "github" 82 + }, 83 + "original": { 84 + "owner": "cachix", 85 + "ref": "rolling", 86 + "repo": "devenv-nixpkgs", 87 + "type": "github" 88 + } 89 + }, 90 + "root": { 91 + "inputs": { 92 + "devenv": "devenv", 93 + "git-hooks": "git-hooks", 94 + "nixpkgs": "nixpkgs", 95 + "pre-commit-hooks": [ 96 + "git-hooks" 97 + ] 98 + } 99 + } 100 + }, 101 + "root": "root", 102 + "version": 7 103 + }
+18
backend/devenv.nix
··· 1 + { 2 + services.postgres = { 3 + enable = true; 4 + initialDatabases = [ 5 + { 6 + name = "supply-product"; 7 + schema = ./schemas/supply-product.sql; 8 + } 9 + ]; 10 + listen_addresses = "127.0.0.1"; 11 + hbaConf = '' 12 + local all all trust 13 + host all all 127.0.0.1/32 trust 14 + ''; 15 + }; 16 + 17 + services.mongodb.enable = true; 18 + }
+64
backend/schemas/supply-product.sql
··· 1 + CREATE SCHEMA SUPPLY_CHAIN; 2 + 3 + CREATE TABLE SUPPLY_CHAIN.SUPPLIER ( 4 + supnr CHAR(4) PRIMARY KEY, 5 + supname VARCHAR(40) NOT NULL, 6 + supaddress VARCHAR(50), 7 + supcity VARCHAR(20), 8 + supstatus SMALLINT CHECK (supstatus >= 0 AND supstatus <= 100), 9 + supcategory VARCHAR(10) DEFAULT 'SILVER' NOT NULL 10 + ); 11 + 12 + CREATE TYPE SUPPLY_CHAIN.product_type AS ENUM ('white', 'red', 'rose', 'sparkling'); 13 + 14 + CREATE TABLE SUPPLY_CHAIN.PRODUCT ( 15 + prodnr CHAR(6) PRIMARY KEY, 16 + prodname VARCHAR(60) UNIQUE NOT NULL, 17 + prodtype SUPPLY_CHAIN.product_type, 18 + available_quantity INTEGER, 19 + prodimage BYTEA 20 + ); 21 + 22 + CREATE TABLE SUPPLY_CHAIN.SUPPLIES ( 23 + supnr CHAR(4) 24 + NOT NULL 25 + REFERENCES SUPPLY_CHAIN.SUPPLIER(supnr) 26 + ON DELETE CASCADE 27 + ON UPDATE CASCADE, 28 + prodnr CHAR(6) 29 + NOT NULL 30 + REFERENCES SUPPLY_CHAIN.PRODUCT(prodnr) 31 + ON DELETE CASCADE 32 + ON UPDATE CASCADE, 33 + purchase_price DECIMAL(8,2), 34 + deliv_period TIME, 35 + PRIMARY KEY (supnr, prodnr) 36 + ); 37 + 38 + COMMENT ON COLUMN SUPPLY_CHAIN.SUPPLIES.purchase_price IS 'in EUR'; 39 + COMMENT ON COLUMN SUPPLY_CHAIN.SUPPLIES.deliv_period IS 'in days'; 40 + 41 + CREATE TABLE SUPPLY_CHAIN.PURCHASE_ORDER ( 42 + ponr CHAR(7) PRIMARY KEY, 43 + podate DATE, 44 + supnr CHAR(4) 45 + NOT NULL 46 + REFERENCES SUPPLY_CHAIN.SUPPLIER(supnr) 47 + ON DELETE CASCADE 48 + ON UPDATE CASCADE 49 + ); 50 + 51 + CREATE TABLE SUPPLY_CHAIN.PO_LINE ( 52 + ponr CHAR(7) 53 + NOT NULL 54 + REFERENCES SUPPLY_CHAIN.PURCHASE_ORDER(ponr) 55 + ON DELETE CASCADE 56 + ON UPDATE CASCADE, 57 + prodnr CHAR(6) 58 + NOT NULL 59 + REFERENCES SUPPLY_CHAIN.PRODUCT(prodnr) 60 + ON DELETE CASCADE 61 + ON UPDATE CASCADE, 62 + quantity INTEGER, 63 + PRIMARY KEY (ponr, prodnr) 64 + );
+19 -2
nilla.nix
··· 8 8 nilla = import pins.nilla; 9 9 in 10 10 nilla.create ( 11 - { config }: 11 + { config, lib }: 12 12 { 13 13 config = { 14 14 inputs = { ··· 124 124 125 125 shell = 126 126 { 127 - pkgs, 128 127 mkShell, 128 + pkgs 129 129 }: 130 130 mkShell { 131 131 shellHook = '' ··· 139 139 pkgs.nixd 140 140 pkgs.nil 141 141 pkgs.nodePackages.live-server 142 + ]; 143 + }; 144 + }; 145 + shells.backend = { 146 + systems = ["x86_64-linux"]; 147 + shell = {mkShell, lib, system}: 148 + let 149 + pkgs = import pins.nixpkgs { 150 + inherit system; 151 + config.allowUnfree = true; 152 + }; 153 + in mkShell { 154 + packages = [ 155 + pkgs.jetbrains.datagrip 156 + pkgs.devenv 157 + pkgs.mongosh 158 + pkgs.mongodb-compass 142 159 ]; 143 160 }; 144 161 };
+84 -90
server/books.ts
··· 3 3 type Request, 4 4 type Response, 5 5 } from "express"; 6 - import { writeFile, readFile, exists } from "fs/promises"; 6 + import { MongoClient } from "mongodb"; 7 7 8 8 const ISBN13 = 9 9 /^(?:ISBN(?:-13)?:? )?(?=[0-9]{13}$|(?=(?:[0-9]+[- ]){4})[- 0-9]{17}$)[\d-]+$/; 10 10 11 11 interface Book { 12 - id: string; 13 12 title: string; 14 - author: string; 13 + authors: string[]; 14 + isbn: string; 15 + publicationYear: number; 16 + genres: string[]; 17 + pageCount: number; 18 + averageRating: number; 19 + numberOfRatings: number; 15 20 } 16 21 17 - const initBooks: () => Promise<void> = async () => { 18 - await writeFile( 19 - "books.json", 20 - JSON.stringify([ 21 - { 22 - id: "9780553212471", 23 - title: "Frankenstein", 24 - author: "Mary Shelley", 25 - }, 26 - { 27 - id: "9780060935467", 28 - title: "To Kill a Mockingbird", 29 - author: "Harper Lee", 30 - }, 31 - { 32 - id: "9780141439518", 33 - title: "Pride and Prejudice", 34 - author: "Jane Austen", 35 - }, 36 - ]), 37 - ); 38 - }; 39 - 40 22 enum ErrorType { 41 23 NotFound, 42 24 InvalidId, 43 25 BadData, 44 26 AlreadyExists, 45 27 } 28 + 29 + const database = new MongoClient("mongodb://localhost:27017") 30 + .db("library") 31 + .collection<Book>("books"); 46 32 47 33 class BookError extends Error { 48 34 public readonly status: number; ··· 74 60 } 75 61 76 62 const getBooks: () => Promise<Book[]> = async () => { 77 - if (!(await exists("books.json"))) { 78 - await initBooks(); 79 - } 80 - const file = await readFile("books.json", "utf-8"); 81 - if (file.length < 4) { 82 - await initBooks(); 83 - return await getBooks(); 84 - } 85 - return JSON.parse(file); 63 + return (await database.find().toArray()).map((b) => b as Book); 86 64 }; 87 65 88 - const updateBook = async (task: Book): Promise<void> => { 89 - const books = await getBooks(); 90 - const index = books.findIndex((b) => b.id === task.id); 91 - if (index !== -1) { 92 - books[index] = task; 93 - } else { 94 - books.push(task); 95 - } 96 - await writeFile("books.json", JSON.stringify(books)); 66 + const updateBook = async ( 67 + isbn: string, 68 + book: Partial<Omit<Book, "isbn">>, 69 + ): Promise<void> => { 70 + database.updateOne({ isbn }, { $set: book }); 97 71 }; 98 72 99 - const removeBook = async (id: string): Promise<void> => { 100 - const books = await getBooks(); 101 - const index = books.findIndex((b) => b.id === id); 102 - if (index !== -1) { 103 - books.splice(index, 1); 104 - await writeFile("books.json", JSON.stringify(books)); 73 + const removeBook = async (isbn: string): Promise<void> => { 74 + if ((await database.deleteOne({ isbn })).deletedCount === 0) { 75 + throw new BookError(ErrorType.NotFound); 105 76 } 106 77 }; 107 78 ··· 123 94 } 124 95 125 96 const keyTypes = { 126 - id: "ISBN13 code", 127 97 title: "string", 128 - author: "string", 98 + author: "string array", 99 + isbn: "ISBN13 code", 100 + publicationYear: "integer", 101 + genres: "string array", 102 + pageCount: "integer", 103 + averageRating: "float 0-10", 104 + numberOfRatings: "integer", 129 105 }; 130 106 131 - const validateBook = (book: { [key: string]: any }): Book => { 132 - let missingKeys = ["id", "title", "author"].filter( 107 + const validateBook = (book: { [key: string]: any }, create: boolean): Book => { 108 + let missingKeys = ["isbn", "title", "authors"].filter( 133 109 (key) => !Object.keys(book).includes(key), 134 110 ); 135 111 let extraKeys = Object.keys(book).filter( 136 - (key) => !["id", "title", "author"].includes(key), 112 + (key) => !["isbn", "title", "authors"].includes(key), 137 113 ); 138 114 let badValues = Object.entries(book) 139 115 .filter(([key, value]) => { 140 - if (key === "id") return typeof value !== "string" || !ISBN13.test(value); 116 + if (key === "isbn") 117 + return typeof value !== "string" || !ISBN13.test(value); 141 118 if (key === "title") return typeof value !== "string"; 142 - if (key === "author") return typeof value !== "string"; 119 + if (key === "authors") 120 + return !( 121 + Array.isArray(value) && 122 + value.every((author) => typeof author === "string") 123 + ); 143 124 return false; 144 125 }) 145 126 .map( 146 127 ([key, _value]) => 147 128 [key, keyTypes[key as keyof typeof keyTypes]] as [string, string], 148 129 ); 149 - if (missingKeys.length > 0 || extraKeys.length > 0 || badValues.length > 0) { 130 + if ( 131 + (create && missingKeys.length > 0) || 132 + extraKeys.length > 0 || 133 + badValues.length > 0 134 + ) { 150 135 throw new BadDataIssues(missingKeys, extraKeys, badValues); 151 136 } 152 137 return book as Book; ··· 169 154 next(); 170 155 }; 171 156 157 + const validateISBN = (isbn: string): string => { 158 + if (!ISBN13.test(isbn)) { 159 + throw new BookError(ErrorType.InvalidId); 160 + } 161 + return isbn.split("").map(Number).join(""); 162 + }; 163 + 172 164 const errorHandler = ( 173 165 err: Error, 174 166 _req: Request, ··· 176 168 _next: NextFunction, 177 169 ) => { 178 170 if (err instanceof BookError) { 179 - let msg = err.message.replace("{{id}}", res.locals.id ?? ""); 171 + let msg = err.message.replace("{{id}}", res.locals.isbn ?? ""); 180 172 181 173 let obj: Map<string, any> = new Map<string, any>([ 182 174 ["error", `${err.name}: ${msg}`], ··· 201 193 } 202 194 }; 203 195 196 + const getISBN = async (req: Request, res: Response, next: NextFunction) => { 197 + const isbn = req.params.isbn; 198 + if (!isbn) { 199 + throw new BookError(ErrorType.InvalidId); 200 + } 201 + try { 202 + const validated = validateISBN(isbn); 203 + res.locals.isbn = validated; 204 + next(); 205 + } catch (err) { 206 + if (err instanceof BookError) { 207 + throw err; 208 + } else { 209 + res.status(500).json({ error: "Internal Server Error" }); 210 + return; 211 + } 212 + } 213 + }; 214 + 204 215 const router = express.Router(); 205 216 206 217 router.use(express.json()); 207 - router.use((req, res, next) => { 218 + router.use((req, _res, next) => { 208 219 console.log(`Recieved a ${req.method} request to ${req.url}`); 209 220 next(); 210 221 }); ··· 217 228 router.post("/", async (req, res) => { 218 229 const books = await getBooks(); 219 230 try { 220 - const bookData = validateBook(req.body); 221 - res.locals.id = bookData.id; 222 - if (books.filter((b) => b.id === bookData.id).length > 0) { 231 + const bookData = validateBook(req.body, true); 232 + res.locals.id = bookData.isbn; 233 + if (books.filter((b) => b.isbn === bookData.isbn).length > 0) { 223 234 throw new BookError(ErrorType.AlreadyExists); 224 235 } 225 - await updateBook(bookData); 236 + await updateBook(res.locals.id, bookData); 226 237 res.status(201).json(bookData); 227 238 } catch (err) { 228 239 if (err instanceof BookError) { ··· 236 247 } 237 248 }); 238 249 239 - router.get("/:id", async (req, res) => { 240 - res.locals.id = req.params.id; 241 - if (!ISBN13.test(req.params.id)) { 242 - throw new BookError(ErrorType.InvalidId); 243 - } 250 + router.get("/:isbn", getISBN, async (_req, res) => { 244 251 const books = await getBooks(); 245 - const book = books.find((b) => b.id == req.params.id); 252 + const book = books.find((b) => b.isbn == res.locals.isbn); 246 253 if (!book) throw new BookError(ErrorType.NotFound); 247 254 res.json(book); 248 255 }); 249 256 250 - router.put("/:id", async (req, res) => { 251 - res.locals.id = req.params.id; 252 - if (!ISBN13.test(req.params.id)) { 253 - throw new BookError(ErrorType.InvalidId); 254 - } 257 + router.patch("/:isbn", getISBN, async (req, res) => { 255 258 const books = await getBooks(); 256 - const book = books.find((b) => b.id == req.params.id); 259 + const book = books.find((b) => b.isbn == res.locals.isbn); 257 260 if (!book) throw new BookError(ErrorType.NotFound); 258 - const bookData = validateBook(req.body); 259 - await updateBook(bookData); 261 + const bookData = validateBook(req.body, false); 262 + await updateBook(res.locals.isbn, bookData); 260 263 res.sendStatus(204); 261 264 }); 262 265 263 - router.delete("/reset", async (_req, res) => { 264 - await initBooks(); 265 - res.sendStatus(204); 266 - }); 267 - 268 - router.delete("/:id", async (req, res) => { 269 - res.locals.id = req.params.id; 270 - if (!ISBN13.test(req.params.id)) { 271 - throw new BookError(ErrorType.InvalidId); 272 - } 266 + router.delete("/:isbn", getISBN, async (_req, res) => { 273 267 const books = await getBooks(); 274 - const book = books.find((b) => b.id == req.params.id); 268 + const book = books.find((b) => b.isbn == res.locals.isbn); 275 269 if (!book) throw new BookError(ErrorType.NotFound); 276 - await removeBook(book.id); 270 + await removeBook(book.isbn); 277 271 res.sendStatus(204); 278 272 }); 279 273
+25
server/bun.lock
··· 6 6 "dependencies": { 7 7 "@types/express": "^5.0.6", 8 8 "express": "^5.2.1", 9 + "mongodb": "^7.0.0", 9 10 }, 10 11 "devDependencies": { 11 12 "@types/bun": "latest", ··· 16 17 }, 17 18 }, 18 19 "packages": { 20 + "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.4", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-p7X/ytJDIdwUfFL/CLOhKgdfJe1Fa8uw9seJYvdOmnP9JBWGWHW69HkOixXS6Wy9yvGf1MbhcS6lVmrhy4jm2g=="], 21 + 19 22 "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], 20 23 21 24 "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], ··· 38 41 39 42 "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], 40 43 44 + "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], 45 + 46 + "@types/whatwg-url": ["@types/whatwg-url@13.0.0", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q=="], 47 + 41 48 "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], 42 49 43 50 "body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="], 51 + 52 + "bson": ["bson@7.0.0", "", {}, "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw=="], 44 53 45 54 "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], 46 55 ··· 112 121 113 122 "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], 114 123 124 + "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], 125 + 115 126 "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], 116 127 117 128 "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], 118 129 119 130 "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], 120 131 132 + "mongodb": ["mongodb@7.0.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^7.0.0", "mongodb-connection-string-url": "^7.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.806.0", "@mongodb-js/zstd": "^7.0.0", "gcp-metadata": "^7.0.1", "kerberos": "^7.0.0", "mongodb-client-encryption": ">=7.0.0 <7.1.0", "snappy": "^7.3.2", "socks": "^2.8.6" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg=="], 133 + 134 + "mongodb-connection-string-url": ["mongodb-connection-string-url@7.0.0", "", { "dependencies": { "@types/whatwg-url": "^13.0.0", "whatwg-url": "^14.1.0" } }, "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og=="], 135 + 121 136 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 122 137 123 138 "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], ··· 133 148 "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], 134 149 135 150 "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], 151 + 152 + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 136 153 137 154 "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], 138 155 ··· 157 174 "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], 158 175 159 176 "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], 177 + 178 + "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], 160 179 161 180 "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], 162 181 163 182 "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], 164 183 184 + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], 185 + 165 186 "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], 166 187 167 188 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], ··· 171 192 "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], 172 193 173 194 "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], 195 + 196 + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], 197 + 198 + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], 174 199 175 200 "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], 176 201 }
+2 -1
server/package.json
··· 11 11 }, 12 12 "dependencies": { 13 13 "@types/express": "^5.0.6", 14 - "express": "^5.2.1" 14 + "express": "^5.2.1", 15 + "mongodb": "^7.0.0" 15 16 } 16 17 }