tangled
alpha
login
or
join now
thecoded.prof
/
CMU
0
fork
atom
CMU Coding Bootcamp
0
fork
atom
overview
issues
pulls
pipelines
feat: backend unit / SQL & NoSQL
thecoded.prof
2 months ago
8cdf1444
be13960b
verified
This commit was signed with the committer's
known signature
.
thecoded.prof
SSH Key Fingerprint:
SHA256:ePn0u8NlJyz3J4Zl9MHOYW3f4XKoi5K1I4j53bwpG0U=
+747
-93
10 changed files
expand all
collapse all
unified
split
.gitignore
backend
.devenv.flake.nix
dec15.sql
devenv.lock
devenv.nix
schemas
supply-product.sql
nilla.nix
server
books.ts
bun.lock
package.json
+1
.gitignore
···
1
1
node_modules
2
2
+
.devenv
+315
backend/.devenv.flake.nix
···
1
1
+
{
2
2
+
inputs =
3
3
+
let
4
4
+
version = "1.9.0";
5
5
+
system = "x86_64-linux";
6
6
+
devenv_root = "/home/coded/Programming/CMU/backend";
7
7
+
devenv_dotfile = "/home/coded/Programming/CMU/backend/.devenv";
8
8
+
devenv_dotfile_path = ./.devenv;
9
9
+
devenv_tmpdir = "/run/user/1000";
10
10
+
devenv_runtime = "/run/user/1000/devenv-684c98d";
11
11
+
devenv_istesting = false;
12
12
+
devenv_direnvrc_latest_version = 1;
13
13
+
container_name = null;
14
14
+
active_profiles = [ ];
15
15
+
hostname = "shorthair";
16
16
+
username = "coded";
17
17
+
18
18
+
in {
19
19
+
git-hooks.url = "github:cachix/git-hooks.nix";
20
20
+
git-hooks.inputs.nixpkgs.follows = "nixpkgs";
21
21
+
pre-commit-hooks.follows = "git-hooks";
22
22
+
nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling";
23
23
+
devenv.url = "github:cachix/devenv?dir=src/modules";
24
24
+
} // (if builtins.pathExists (devenv_dotfile_path + "/flake.json")
25
25
+
then builtins.fromJSON (builtins.readFile (devenv_dotfile_path + "/flake.json"))
26
26
+
else { });
27
27
+
28
28
+
outputs = { nixpkgs, ... }@inputs:
29
29
+
let
30
30
+
version = "1.9.0";
31
31
+
system = "x86_64-linux";
32
32
+
devenv_root = "/home/coded/Programming/CMU/backend";
33
33
+
devenv_dotfile = "/home/coded/Programming/CMU/backend/.devenv";
34
34
+
devenv_dotfile_path = ./.devenv;
35
35
+
devenv_tmpdir = "/run/user/1000";
36
36
+
devenv_runtime = "/run/user/1000/devenv-684c98d";
37
37
+
devenv_istesting = false;
38
38
+
devenv_direnvrc_latest_version = 1;
39
39
+
container_name = null;
40
40
+
active_profiles = [ ];
41
41
+
hostname = "shorthair";
42
42
+
username = "coded";
43
43
+
44
44
+
devenv =
45
45
+
if builtins.pathExists (devenv_dotfile_path + "/devenv.json")
46
46
+
then builtins.fromJSON (builtins.readFile (devenv_dotfile_path + "/devenv.json"))
47
47
+
else { };
48
48
+
49
49
+
systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
50
50
+
51
51
+
# Function to create devenv configuration for a specific system with profiles support
52
52
+
mkDevenvForSystem = targetSystem:
53
53
+
let
54
54
+
getOverlays = inputName: inputAttrs:
55
55
+
map
56
56
+
(overlay:
57
57
+
let
58
58
+
input = inputs.${inputName} or (throw "No such input `${inputName}` while trying to configure overlays.");
59
59
+
in
60
60
+
input.overlays.${overlay} or (throw "Input `${inputName}` has no overlay called `${overlay}`. Supported overlays: ${nixpkgs.lib.concatStringsSep ", " (builtins.attrNames input.overlays)}"))
61
61
+
inputAttrs.overlays or [ ];
62
62
+
overlays = nixpkgs.lib.flatten (nixpkgs.lib.mapAttrsToList getOverlays (devenv.inputs or { }));
63
63
+
permittedUnfreePackages = devenv.nixpkgs.per-platform."${targetSystem}".permittedUnfreePackages or devenv.nixpkgs.permittedUnfreePackages or [ ];
64
64
+
pkgs = import nixpkgs {
65
65
+
system = targetSystem;
66
66
+
config = {
67
67
+
allowUnfree = devenv.nixpkgs.per-platform."${targetSystem}".allowUnfree or devenv.nixpkgs.allowUnfree or devenv.allowUnfree or false;
68
68
+
allowBroken = devenv.nixpkgs.per-platform."${targetSystem}".allowBroken or devenv.nixpkgs.allowBroken or devenv.allowBroken or false;
69
69
+
cudaSupport = devenv.nixpkgs.per-platform."${targetSystem}".cudaSupport or devenv.nixpkgs.cudaSupport or false;
70
70
+
cudaCapabilities = devenv.nixpkgs.per-platform."${targetSystem}".cudaCapabilities or devenv.nixpkgs.cudaCapabilities or [ ];
71
71
+
permittedInsecurePackages = devenv.nixpkgs.per-platform."${targetSystem}".permittedInsecurePackages or devenv.nixpkgs.permittedInsecurePackages or devenv.permittedInsecurePackages or [ ];
72
72
+
allowUnfreePredicate = if (permittedUnfreePackages != [ ]) then (pkg: builtins.elem (nixpkgs.lib.getName pkg) permittedUnfreePackages) else (_: false);
73
73
+
};
74
74
+
inherit overlays;
75
75
+
};
76
76
+
lib = pkgs.lib;
77
77
+
importModule = path:
78
78
+
if lib.hasPrefix "./" path
79
79
+
then if lib.hasSuffix ".nix" path
80
80
+
then ./. + (builtins.substring 1 255 path)
81
81
+
else ./. + (builtins.substring 1 255 path) + "/devenv.nix"
82
82
+
else if lib.hasPrefix "../" path
83
83
+
then throw "devenv: ../ is not supported for imports"
84
84
+
else
85
85
+
let
86
86
+
paths = lib.splitString "/" path;
87
87
+
name = builtins.head paths;
88
88
+
input = inputs.${name} or (throw "Unknown input ${name}");
89
89
+
subpath = "/${lib.concatStringsSep "/" (builtins.tail paths)}";
90
90
+
devenvpath = "${input}" + subpath;
91
91
+
devenvdefaultpath = devenvpath + "/devenv.nix";
92
92
+
in
93
93
+
if lib.hasSuffix ".nix" devenvpath
94
94
+
then devenvpath
95
95
+
else if builtins.pathExists devenvdefaultpath
96
96
+
then devenvdefaultpath
97
97
+
else throw (devenvdefaultpath + " file does not exist for input ${name}.");
98
98
+
99
99
+
# Phase 1: Base evaluation to extract profile definitions
100
100
+
baseProject = pkgs.lib.evalModules {
101
101
+
specialArgs = inputs // { inherit inputs; };
102
102
+
modules = [
103
103
+
({ config, ... }: {
104
104
+
_module.args.pkgs = pkgs.appendOverlays (config.overlays or [ ]);
105
105
+
})
106
106
+
(inputs.devenv.modules + /top-level.nix)
107
107
+
{
108
108
+
devenv.cliVersion = version;
109
109
+
devenv.root = devenv_root;
110
110
+
devenv.dotfile = devenv_dotfile;
111
111
+
}
112
112
+
({ options, ... }: {
113
113
+
config.devenv = lib.mkMerge [
114
114
+
(pkgs.lib.optionalAttrs (builtins.hasAttr "tmpdir" options.devenv) {
115
115
+
tmpdir = devenv_tmpdir;
116
116
+
})
117
117
+
(pkgs.lib.optionalAttrs (builtins.hasAttr "isTesting" options.devenv) {
118
118
+
isTesting = devenv_istesting;
119
119
+
})
120
120
+
(pkgs.lib.optionalAttrs (builtins.hasAttr "runtime" options.devenv) {
121
121
+
runtime = devenv_runtime;
122
122
+
})
123
123
+
(pkgs.lib.optionalAttrs (builtins.hasAttr "direnvrcLatestVersion" options.devenv) {
124
124
+
direnvrcLatestVersion = devenv_direnvrc_latest_version;
125
125
+
})
126
126
+
];
127
127
+
})
128
128
+
(pkgs.lib.optionalAttrs (container_name != null) {
129
129
+
container.isBuilding = pkgs.lib.mkForce true;
130
130
+
containers.${container_name}.isBuilding = true;
131
131
+
})
132
132
+
] ++ (map importModule (devenv.imports or [ ])) ++ [
133
133
+
(if builtins.pathExists ./devenv.nix then ./devenv.nix else { })
134
134
+
(devenv.devenv or { })
135
135
+
(if builtins.pathExists ./devenv.local.nix then ./devenv.local.nix else { })
136
136
+
(if builtins.pathExists (devenv_dotfile_path + "/cli-options.nix") then import (devenv_dotfile_path + "/cli-options.nix") else { })
137
137
+
];
138
138
+
};
139
139
+
140
140
+
# Phase 2: Extract and apply profiles using extendModules with priority overrides
141
141
+
project =
142
142
+
let
143
143
+
# Build ordered list of profile names: hostname -> user -> manual
144
144
+
manualProfiles = active_profiles;
145
145
+
currentHostname = hostname;
146
146
+
currentUsername = username;
147
147
+
hostnameProfiles = lib.optional (currentHostname != "" && builtins.hasAttr currentHostname (baseProject.config.profiles.hostname or { })) "hostname.${currentHostname}";
148
148
+
userProfiles = lib.optional (currentUsername != "" && builtins.hasAttr currentUsername (baseProject.config.profiles.user or { })) "user.${currentUsername}";
149
149
+
150
150
+
# Ordered list of profiles to activate
151
151
+
orderedProfiles = hostnameProfiles ++ userProfiles ++ manualProfiles;
152
152
+
153
153
+
# Resolve profile extends with cycle detection
154
154
+
resolveProfileExtends = profileName: visited:
155
155
+
if builtins.elem profileName visited then
156
156
+
throw "Circular dependency detected in profile extends: ${lib.concatStringsSep " -> " visited} -> ${profileName}"
157
157
+
else
158
158
+
let
159
159
+
profile = getProfileConfig profileName;
160
160
+
extends = profile.extends or [ ];
161
161
+
newVisited = visited ++ [ profileName ];
162
162
+
extendedProfiles = lib.flatten (map (name: resolveProfileExtends name newVisited) extends);
163
163
+
in
164
164
+
extendedProfiles ++ [ profileName ];
165
165
+
166
166
+
# Get profile configuration by name from baseProject
167
167
+
getProfileConfig = profileName:
168
168
+
if lib.hasPrefix "hostname." profileName then
169
169
+
let name = lib.removePrefix "hostname." profileName;
170
170
+
in baseProject.config.profiles.hostname.${name}
171
171
+
else if lib.hasPrefix "user." profileName then
172
172
+
let name = lib.removePrefix "user." profileName;
173
173
+
in baseProject.config.profiles.user.${name}
174
174
+
else
175
175
+
let
176
176
+
availableProfiles = builtins.attrNames (baseProject.config.profiles or { });
177
177
+
hostnameProfiles = map (n: "hostname.${n}") (builtins.attrNames (baseProject.config.profiles.hostname or { }));
178
178
+
userProfiles = map (n: "user.${n}") (builtins.attrNames (baseProject.config.profiles.user or { }));
179
179
+
allAvailableProfiles = availableProfiles ++ hostnameProfiles ++ userProfiles;
180
180
+
in
181
181
+
baseProject.config.profiles.${profileName} or (throw "Profile '${profileName}' not found. Available profiles: ${lib.concatStringsSep ", " allAvailableProfiles}");
182
182
+
183
183
+
# Fold over ordered profiles to build final list with extends
184
184
+
expandedProfiles = lib.foldl'
185
185
+
(acc: profileName:
186
186
+
let
187
187
+
allProfileNames = resolveProfileExtends profileName [ ];
188
188
+
in
189
189
+
acc ++ allProfileNames
190
190
+
) [ ]
191
191
+
orderedProfiles;
192
192
+
193
193
+
# Map over expanded profiles and apply priorities
194
194
+
allPrioritizedModules = lib.imap0
195
195
+
(index: profileName:
196
196
+
let
197
197
+
# Decrement priority for each profile (lower = higher precedence)
198
198
+
# Start with the next lowest priority after the default priority for values (100)
199
199
+
profilePriority = (lib.modules.defaultOverridePriority - 1) - index;
200
200
+
profileConfig = getProfileConfig profileName;
201
201
+
202
202
+
# Support overriding both plain attrset modules and functions
203
203
+
applyModuleOverride = config:
204
204
+
if builtins.isFunction config
205
205
+
then
206
206
+
let
207
207
+
wrapper = args: applyOverrideRecursive (config args);
208
208
+
in
209
209
+
lib.mirrorFunctionArgs config wrapper
210
210
+
else applyOverrideRecursive config;
211
211
+
212
212
+
# Apply overrides recursively
213
213
+
applyOverrideRecursive = config:
214
214
+
if lib.isAttrs config && config ? _type
215
215
+
then config # Don't override values with existing type metadata
216
216
+
else if lib.isAttrs config
217
217
+
then lib.mapAttrs (_: applyOverrideRecursive) config
218
218
+
else lib.mkOverride profilePriority config;
219
219
+
220
220
+
# Apply priority overrides recursively to the deferredModule imports structure
221
221
+
prioritizedConfig = (
222
222
+
profileConfig.module // {
223
223
+
imports = lib.map
224
224
+
(importItem:
225
225
+
importItem // {
226
226
+
imports = lib.map
227
227
+
(nestedImport:
228
228
+
applyModuleOverride nestedImport
229
229
+
)
230
230
+
(importItem.imports or [ ]);
231
231
+
}
232
232
+
)
233
233
+
(profileConfig.module.imports or [ ]);
234
234
+
}
235
235
+
);
236
236
+
in
237
237
+
prioritizedConfig
238
238
+
)
239
239
+
expandedProfiles;
240
240
+
in
241
241
+
if allPrioritizedModules == [ ]
242
242
+
then baseProject
243
243
+
else baseProject.extendModules { modules = allPrioritizedModules; };
244
244
+
245
245
+
config = project.config;
246
246
+
247
247
+
options = pkgs.nixosOptionsDoc {
248
248
+
options = builtins.removeAttrs project.options [ "_module" ];
249
249
+
warningsAreErrors = false;
250
250
+
# Unpack Nix types, e.g. literalExpression, mDoc.
251
251
+
transformOptions =
252
252
+
let isDocType = v: builtins.elem v [ "literalDocBook" "literalExpression" "literalMD" "mdDoc" ];
253
253
+
in lib.attrsets.mapAttrs (_: v:
254
254
+
if v ? _type && isDocType v._type then
255
255
+
v.text
256
256
+
else if v ? _type && v._type == "derivation" then
257
257
+
v.name
258
258
+
else
259
259
+
v
260
260
+
);
261
261
+
};
262
262
+
263
263
+
# Recursively search for outputs in the config.
264
264
+
# This is used when not building a specific output by attrpath.
265
265
+
build = options: config:
266
266
+
lib.concatMapAttrs
267
267
+
(name: option:
268
268
+
if lib.isOption option then
269
269
+
let typeName = option.type.name or "";
270
270
+
in
271
271
+
if builtins.elem typeName [ "output" "outputOf" ] then
272
272
+
{ ${name} = config.${name}; }
273
273
+
else { }
274
274
+
else if builtins.isAttrs option && !lib.isDerivation option then
275
275
+
let v = build option config.${name};
276
276
+
in if v != { } then {
277
277
+
${name} = v;
278
278
+
} else { }
279
279
+
else { }
280
280
+
)
281
281
+
options;
282
282
+
in
283
283
+
{
284
284
+
inherit config options build;
285
285
+
shell = config.shell;
286
286
+
packages = {
287
287
+
optionsJSON = options.optionsJSON;
288
288
+
# deprecated
289
289
+
inherit (config) info procfileScript procfileEnv procfile;
290
290
+
ci = config.ciDerivation;
291
291
+
};
292
292
+
};
293
293
+
294
294
+
# Generate per-system devenv configurations
295
295
+
perSystem = nixpkgs.lib.genAttrs systems mkDevenvForSystem;
296
296
+
297
297
+
# Default devenv for the current system
298
298
+
currentSystemDevenv = perSystem.${system};
299
299
+
in
300
300
+
{
301
301
+
devShell = nixpkgs.lib.genAttrs systems (s: perSystem.${s}.shell);
302
302
+
packages = nixpkgs.lib.genAttrs systems (s: perSystem.${s}.packages);
303
303
+
304
304
+
# Per-system devenv configurations
305
305
+
devenv = {
306
306
+
# Default devenv for the current system
307
307
+
inherit (currentSystemDevenv) config options build shell packages;
308
308
+
# Per-system devenv configurations
309
309
+
inherit perSystem;
310
310
+
};
311
311
+
312
312
+
# Legacy build output
313
313
+
build = currentSystemDevenv.build currentSystemDevenv.options currentSystemDevenv.config;
314
314
+
};
315
315
+
}
+116
backend/dec15.sql
···
1
1
+
-- CREATE
2
2
+
INSERT INTO
3
3
+
SUPPLY_CHAIN.SUPPLIER
4
4
+
VALUES
5
5
+
(
6
6
+
'amaz',
7
7
+
'amazon',
8
8
+
'410 Terry Ave N',
9
9
+
'Seattle',
10
10
+
0,
11
11
+
'SILVER'
12
12
+
),
13
13
+
(
14
14
+
'goog',
15
15
+
'google',
16
16
+
'1600 Amphitheatre Parkway',
17
17
+
'Mountain View',
18
18
+
0,
19
19
+
'GOLD'
20
20
+
);
21
21
+
22
22
+
INSERT INTO
23
23
+
SUPPLY_CHAIN.PRODUCT
24
24
+
VALUES
25
25
+
('whwine', 'white wine', 'white', 100, ''),
26
26
+
('rewine', 'red wine', 'red', 150, ''),
27
27
+
('rowine', 'rose wine', 'rose', 0, ''),
28
28
+
('prosec', 'prosecco', 'sparkling', 500, ''),
29
29
+
('clubso', 'club soda', 'sparkling', 200, '');
30
30
+
31
31
+
INSERT INTO
32
32
+
SUPPLY_CHAIN.SUPPLIES
33
33
+
VALUES
34
34
+
('amaz', 'whwine', 10.22, '17:00'),
35
35
+
('goog', 'rowine', 5.16, '23:00'),
36
36
+
('amaz', 'rewine', 7.12, '19:00'),
37
37
+
('goog', 'clubso', 3.07, '03:00'),
38
38
+
('amaz', 'prosec', 9.99, '05:00');
39
39
+
40
40
+
INSERT INTO
41
41
+
SUPPLY_CHAIN.PURCHASE_ORDER
42
42
+
VALUES
43
43
+
('aaaaaaa', '2005-01-01', 'amaz'),
44
44
+
('bbbbbbb', '2017-05-24', 'goog'),
45
45
+
('ccccccc', '2013-09-30', 'amaz'),
46
46
+
('ddddddd', '2020-12-01', 'goog'),
47
47
+
('eeeeeee', '2025-12-16', 'amaz');
48
48
+
49
49
+
INSERT INTO
50
50
+
SUPPLY_CHAIN.PO_LINE
51
51
+
VALUES
52
52
+
('aaaaaaa', 'whwine', 5),
53
53
+
('bbbbbbb', 'rowine', 50),
54
54
+
('ccccccc', 'rewine', 10),
55
55
+
('ddddddd', 'clubso', 150),
56
56
+
('eeeeeee', 'prosec', 200);
57
57
+
58
58
+
-- READ
59
59
+
SELECT
60
60
+
*
61
61
+
FROM
62
62
+
SUPPLY_CHAIN.SUPPLIER;
63
63
+
64
64
+
SELECT
65
65
+
*
66
66
+
FROM
67
67
+
SUPPLY_CHAIN.PRODUCT;
68
68
+
69
69
+
SELECT
70
70
+
*
71
71
+
FROM
72
72
+
SUPPLY_CHAIN.SUPPLIES;
73
73
+
74
74
+
SELECT
75
75
+
*
76
76
+
FROM
77
77
+
SUPPLY_CHAIN.PURCHASE_ORDER;
78
78
+
79
79
+
SELECT
80
80
+
P.PRODNR,
81
81
+
P.PRODNAME,
82
82
+
P.PRODTYPE,
83
83
+
P.AVAILABLE_QUANTITY,
84
84
+
PL.PONR,
85
85
+
PL.QUANTITY
86
86
+
FROM
87
87
+
SUPPLY_CHAIN.PRODUCT P
88
88
+
RIGHT JOIN SUPPLY_CHAIN.PO_LINE PL ON P.PRODNR = PL.PRODNR
89
89
+
WHERE
90
90
+
P.AVAILABLE_QUANTITY > 10;
91
91
+
92
92
+
-- UPDATE
93
93
+
UPDATE SUPPLY_CHAIN.supplier
94
94
+
SET
95
95
+
SUPSTATUS = 100
96
96
+
WHERE
97
97
+
SUPNR = 'amaz';
98
98
+
99
99
+
UPDATE SUPPLY_CHAIN.PRODUCT
100
100
+
SET
101
101
+
"available_quantity" = 10
102
102
+
WHERE
103
103
+
AVAILABLE_QUANTITY < 10;
104
104
+
105
105
+
-- DELETE
106
106
+
DELETE FROM SUPPLY_CHAIN.PURCHASE_ORDER WHERE PONR = 'aaaaaaa';
107
107
+
108
108
+
DELETE FROM SUPPLY_CHAIN.SUPPLIER;
109
109
+
110
110
+
DELETE FROM SUPPLY_CHAIN.PRODUCT;
111
111
+
112
112
+
DELETE FROM SUPPLY_CHAIN.PO_LINE;
113
113
+
114
114
+
DELETE FROM SUPPLY_CHAIN.PURCHASE_ORDER;
115
115
+
116
116
+
DELETE FROM SUPPLY_CHAIN.SUPPLIES;
+103
backend/devenv.lock
···
1
1
+
{
2
2
+
"nodes": {
3
3
+
"devenv": {
4
4
+
"locked": {
5
5
+
"dir": "src/modules",
6
6
+
"lastModified": 1765898645,
7
7
+
"owner": "cachix",
8
8
+
"repo": "devenv",
9
9
+
"rev": "b1ca338a97209fa6cdd18dede3f82e284cbcfb0d",
10
10
+
"type": "github"
11
11
+
},
12
12
+
"original": {
13
13
+
"dir": "src/modules",
14
14
+
"owner": "cachix",
15
15
+
"repo": "devenv",
16
16
+
"type": "github"
17
17
+
}
18
18
+
},
19
19
+
"flake-compat": {
20
20
+
"flake": false,
21
21
+
"locked": {
22
22
+
"lastModified": 1765121682,
23
23
+
"owner": "edolstra",
24
24
+
"repo": "flake-compat",
25
25
+
"rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3",
26
26
+
"type": "github"
27
27
+
},
28
28
+
"original": {
29
29
+
"owner": "edolstra",
30
30
+
"repo": "flake-compat",
31
31
+
"type": "github"
32
32
+
}
33
33
+
},
34
34
+
"git-hooks": {
35
35
+
"inputs": {
36
36
+
"flake-compat": "flake-compat",
37
37
+
"gitignore": "gitignore",
38
38
+
"nixpkgs": [
39
39
+
"nixpkgs"
40
40
+
]
41
41
+
},
42
42
+
"locked": {
43
43
+
"lastModified": 1765464257,
44
44
+
"owner": "cachix",
45
45
+
"repo": "git-hooks.nix",
46
46
+
"rev": "09e45f2598e1a8499c3594fe11ec2943f34fe509",
47
47
+
"type": "github"
48
48
+
},
49
49
+
"original": {
50
50
+
"owner": "cachix",
51
51
+
"repo": "git-hooks.nix",
52
52
+
"type": "github"
53
53
+
}
54
54
+
},
55
55
+
"gitignore": {
56
56
+
"inputs": {
57
57
+
"nixpkgs": [
58
58
+
"git-hooks",
59
59
+
"nixpkgs"
60
60
+
]
61
61
+
},
62
62
+
"locked": {
63
63
+
"lastModified": 1762808025,
64
64
+
"owner": "hercules-ci",
65
65
+
"repo": "gitignore.nix",
66
66
+
"rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c",
67
67
+
"type": "github"
68
68
+
},
69
69
+
"original": {
70
70
+
"owner": "hercules-ci",
71
71
+
"repo": "gitignore.nix",
72
72
+
"type": "github"
73
73
+
}
74
74
+
},
75
75
+
"nixpkgs": {
76
76
+
"locked": {
77
77
+
"lastModified": 1764580874,
78
78
+
"owner": "cachix",
79
79
+
"repo": "devenv-nixpkgs",
80
80
+
"rev": "dcf61356c3ab25f1362b4a4428a6d871e84f1d1d",
81
81
+
"type": "github"
82
82
+
},
83
83
+
"original": {
84
84
+
"owner": "cachix",
85
85
+
"ref": "rolling",
86
86
+
"repo": "devenv-nixpkgs",
87
87
+
"type": "github"
88
88
+
}
89
89
+
},
90
90
+
"root": {
91
91
+
"inputs": {
92
92
+
"devenv": "devenv",
93
93
+
"git-hooks": "git-hooks",
94
94
+
"nixpkgs": "nixpkgs",
95
95
+
"pre-commit-hooks": [
96
96
+
"git-hooks"
97
97
+
]
98
98
+
}
99
99
+
}
100
100
+
},
101
101
+
"root": "root",
102
102
+
"version": 7
103
103
+
}
+18
backend/devenv.nix
···
1
1
+
{
2
2
+
services.postgres = {
3
3
+
enable = true;
4
4
+
initialDatabases = [
5
5
+
{
6
6
+
name = "supply-product";
7
7
+
schema = ./schemas/supply-product.sql;
8
8
+
}
9
9
+
];
10
10
+
listen_addresses = "127.0.0.1";
11
11
+
hbaConf = ''
12
12
+
local all all trust
13
13
+
host all all 127.0.0.1/32 trust
14
14
+
'';
15
15
+
};
16
16
+
17
17
+
services.mongodb.enable = true;
18
18
+
}
+64
backend/schemas/supply-product.sql
···
1
1
+
CREATE SCHEMA SUPPLY_CHAIN;
2
2
+
3
3
+
CREATE TABLE SUPPLY_CHAIN.SUPPLIER (
4
4
+
supnr CHAR(4) PRIMARY KEY,
5
5
+
supname VARCHAR(40) NOT NULL,
6
6
+
supaddress VARCHAR(50),
7
7
+
supcity VARCHAR(20),
8
8
+
supstatus SMALLINT CHECK (supstatus >= 0 AND supstatus <= 100),
9
9
+
supcategory VARCHAR(10) DEFAULT 'SILVER' NOT NULL
10
10
+
);
11
11
+
12
12
+
CREATE TYPE SUPPLY_CHAIN.product_type AS ENUM ('white', 'red', 'rose', 'sparkling');
13
13
+
14
14
+
CREATE TABLE SUPPLY_CHAIN.PRODUCT (
15
15
+
prodnr CHAR(6) PRIMARY KEY,
16
16
+
prodname VARCHAR(60) UNIQUE NOT NULL,
17
17
+
prodtype SUPPLY_CHAIN.product_type,
18
18
+
available_quantity INTEGER,
19
19
+
prodimage BYTEA
20
20
+
);
21
21
+
22
22
+
CREATE TABLE SUPPLY_CHAIN.SUPPLIES (
23
23
+
supnr CHAR(4)
24
24
+
NOT NULL
25
25
+
REFERENCES SUPPLY_CHAIN.SUPPLIER(supnr)
26
26
+
ON DELETE CASCADE
27
27
+
ON UPDATE CASCADE,
28
28
+
prodnr CHAR(6)
29
29
+
NOT NULL
30
30
+
REFERENCES SUPPLY_CHAIN.PRODUCT(prodnr)
31
31
+
ON DELETE CASCADE
32
32
+
ON UPDATE CASCADE,
33
33
+
purchase_price DECIMAL(8,2),
34
34
+
deliv_period TIME,
35
35
+
PRIMARY KEY (supnr, prodnr)
36
36
+
);
37
37
+
38
38
+
COMMENT ON COLUMN SUPPLY_CHAIN.SUPPLIES.purchase_price IS 'in EUR';
39
39
+
COMMENT ON COLUMN SUPPLY_CHAIN.SUPPLIES.deliv_period IS 'in days';
40
40
+
41
41
+
CREATE TABLE SUPPLY_CHAIN.PURCHASE_ORDER (
42
42
+
ponr CHAR(7) PRIMARY KEY,
43
43
+
podate DATE,
44
44
+
supnr CHAR(4)
45
45
+
NOT NULL
46
46
+
REFERENCES SUPPLY_CHAIN.SUPPLIER(supnr)
47
47
+
ON DELETE CASCADE
48
48
+
ON UPDATE CASCADE
49
49
+
);
50
50
+
51
51
+
CREATE TABLE SUPPLY_CHAIN.PO_LINE (
52
52
+
ponr CHAR(7)
53
53
+
NOT NULL
54
54
+
REFERENCES SUPPLY_CHAIN.PURCHASE_ORDER(ponr)
55
55
+
ON DELETE CASCADE
56
56
+
ON UPDATE CASCADE,
57
57
+
prodnr CHAR(6)
58
58
+
NOT NULL
59
59
+
REFERENCES SUPPLY_CHAIN.PRODUCT(prodnr)
60
60
+
ON DELETE CASCADE
61
61
+
ON UPDATE CASCADE,
62
62
+
quantity INTEGER,
63
63
+
PRIMARY KEY (ponr, prodnr)
64
64
+
);
+19
-2
nilla.nix
···
8
8
nilla = import pins.nilla;
9
9
in
10
10
nilla.create (
11
11
-
{ config }:
11
11
+
{ config, lib }:
12
12
{
13
13
config = {
14
14
inputs = {
···
124
124
125
125
shell =
126
126
{
127
127
-
pkgs,
128
127
mkShell,
128
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
142
+
];
143
143
+
};
144
144
+
};
145
145
+
shells.backend = {
146
146
+
systems = ["x86_64-linux"];
147
147
+
shell = {mkShell, lib, system}:
148
148
+
let
149
149
+
pkgs = import pins.nixpkgs {
150
150
+
inherit system;
151
151
+
config.allowUnfree = true;
152
152
+
};
153
153
+
in mkShell {
154
154
+
packages = [
155
155
+
pkgs.jetbrains.datagrip
156
156
+
pkgs.devenv
157
157
+
pkgs.mongosh
158
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
6
-
import { writeFile, readFile, exists } from "fs/promises";
6
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
12
-
id: string;
13
12
title: string;
14
14
-
author: string;
13
13
+
authors: string[];
14
14
+
isbn: string;
15
15
+
publicationYear: number;
16
16
+
genres: string[];
17
17
+
pageCount: number;
18
18
+
averageRating: number;
19
19
+
numberOfRatings: number;
15
20
}
16
21
17
17
-
const initBooks: () => Promise<void> = async () => {
18
18
-
await writeFile(
19
19
-
"books.json",
20
20
-
JSON.stringify([
21
21
-
{
22
22
-
id: "9780553212471",
23
23
-
title: "Frankenstein",
24
24
-
author: "Mary Shelley",
25
25
-
},
26
26
-
{
27
27
-
id: "9780060935467",
28
28
-
title: "To Kill a Mockingbird",
29
29
-
author: "Harper Lee",
30
30
-
},
31
31
-
{
32
32
-
id: "9780141439518",
33
33
-
title: "Pride and Prejudice",
34
34
-
author: "Jane Austen",
35
35
-
},
36
36
-
]),
37
37
-
);
38
38
-
};
39
39
-
40
22
enum ErrorType {
41
23
NotFound,
42
24
InvalidId,
43
25
BadData,
44
26
AlreadyExists,
45
27
}
28
28
+
29
29
+
const database = new MongoClient("mongodb://localhost:27017")
30
30
+
.db("library")
31
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
77
-
if (!(await exists("books.json"))) {
78
78
-
await initBooks();
79
79
-
}
80
80
-
const file = await readFile("books.json", "utf-8");
81
81
-
if (file.length < 4) {
82
82
-
await initBooks();
83
83
-
return await getBooks();
84
84
-
}
85
85
-
return JSON.parse(file);
63
63
+
return (await database.find().toArray()).map((b) => b as Book);
86
64
};
87
65
88
88
-
const updateBook = async (task: Book): Promise<void> => {
89
89
-
const books = await getBooks();
90
90
-
const index = books.findIndex((b) => b.id === task.id);
91
91
-
if (index !== -1) {
92
92
-
books[index] = task;
93
93
-
} else {
94
94
-
books.push(task);
95
95
-
}
96
96
-
await writeFile("books.json", JSON.stringify(books));
66
66
+
const updateBook = async (
67
67
+
isbn: string,
68
68
+
book: Partial<Omit<Book, "isbn">>,
69
69
+
): Promise<void> => {
70
70
+
database.updateOne({ isbn }, { $set: book });
97
71
};
98
72
99
99
-
const removeBook = async (id: string): Promise<void> => {
100
100
-
const books = await getBooks();
101
101
-
const index = books.findIndex((b) => b.id === id);
102
102
-
if (index !== -1) {
103
103
-
books.splice(index, 1);
104
104
-
await writeFile("books.json", JSON.stringify(books));
73
73
+
const removeBook = async (isbn: string): Promise<void> => {
74
74
+
if ((await database.deleteOne({ isbn })).deletedCount === 0) {
75
75
+
throw new BookError(ErrorType.NotFound);
105
76
}
106
77
};
107
78
···
123
94
}
124
95
125
96
const keyTypes = {
126
126
-
id: "ISBN13 code",
127
97
title: "string",
128
128
-
author: "string",
98
98
+
author: "string array",
99
99
+
isbn: "ISBN13 code",
100
100
+
publicationYear: "integer",
101
101
+
genres: "string array",
102
102
+
pageCount: "integer",
103
103
+
averageRating: "float 0-10",
104
104
+
numberOfRatings: "integer",
129
105
};
130
106
131
131
-
const validateBook = (book: { [key: string]: any }): Book => {
132
132
-
let missingKeys = ["id", "title", "author"].filter(
107
107
+
const validateBook = (book: { [key: string]: any }, create: boolean): Book => {
108
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
136
-
(key) => !["id", "title", "author"].includes(key),
112
112
+
(key) => !["isbn", "title", "authors"].includes(key),
137
113
);
138
114
let badValues = Object.entries(book)
139
115
.filter(([key, value]) => {
140
140
-
if (key === "id") return typeof value !== "string" || !ISBN13.test(value);
116
116
+
if (key === "isbn")
117
117
+
return typeof value !== "string" || !ISBN13.test(value);
141
118
if (key === "title") return typeof value !== "string";
142
142
-
if (key === "author") return typeof value !== "string";
119
119
+
if (key === "authors")
120
120
+
return !(
121
121
+
Array.isArray(value) &&
122
122
+
value.every((author) => typeof author === "string")
123
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
149
-
if (missingKeys.length > 0 || extraKeys.length > 0 || badValues.length > 0) {
130
130
+
if (
131
131
+
(create && missingKeys.length > 0) ||
132
132
+
extraKeys.length > 0 ||
133
133
+
badValues.length > 0
134
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
157
+
const validateISBN = (isbn: string): string => {
158
158
+
if (!ISBN13.test(isbn)) {
159
159
+
throw new BookError(ErrorType.InvalidId);
160
160
+
}
161
161
+
return isbn.split("").map(Number).join("");
162
162
+
};
163
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
179
-
let msg = err.message.replace("{{id}}", res.locals.id ?? "");
171
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
196
+
const getISBN = async (req: Request, res: Response, next: NextFunction) => {
197
197
+
const isbn = req.params.isbn;
198
198
+
if (!isbn) {
199
199
+
throw new BookError(ErrorType.InvalidId);
200
200
+
}
201
201
+
try {
202
202
+
const validated = validateISBN(isbn);
203
203
+
res.locals.isbn = validated;
204
204
+
next();
205
205
+
} catch (err) {
206
206
+
if (err instanceof BookError) {
207
207
+
throw err;
208
208
+
} else {
209
209
+
res.status(500).json({ error: "Internal Server Error" });
210
210
+
return;
211
211
+
}
212
212
+
}
213
213
+
};
214
214
+
204
215
const router = express.Router();
205
216
206
217
router.use(express.json());
207
207
-
router.use((req, res, next) => {
218
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
220
-
const bookData = validateBook(req.body);
221
221
-
res.locals.id = bookData.id;
222
222
-
if (books.filter((b) => b.id === bookData.id).length > 0) {
231
231
+
const bookData = validateBook(req.body, true);
232
232
+
res.locals.id = bookData.isbn;
233
233
+
if (books.filter((b) => b.isbn === bookData.isbn).length > 0) {
223
234
throw new BookError(ErrorType.AlreadyExists);
224
235
}
225
225
-
await updateBook(bookData);
236
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
239
-
router.get("/:id", async (req, res) => {
240
240
-
res.locals.id = req.params.id;
241
241
-
if (!ISBN13.test(req.params.id)) {
242
242
-
throw new BookError(ErrorType.InvalidId);
243
243
-
}
250
250
+
router.get("/:isbn", getISBN, async (_req, res) => {
244
251
const books = await getBooks();
245
245
-
const book = books.find((b) => b.id == req.params.id);
252
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
250
-
router.put("/:id", async (req, res) => {
251
251
-
res.locals.id = req.params.id;
252
252
-
if (!ISBN13.test(req.params.id)) {
253
253
-
throw new BookError(ErrorType.InvalidId);
254
254
-
}
257
257
+
router.patch("/:isbn", getISBN, async (req, res) => {
255
258
const books = await getBooks();
256
256
-
const book = books.find((b) => b.id == req.params.id);
259
259
+
const book = books.find((b) => b.isbn == res.locals.isbn);
257
260
if (!book) throw new BookError(ErrorType.NotFound);
258
258
-
const bookData = validateBook(req.body);
259
259
-
await updateBook(bookData);
261
261
+
const bookData = validateBook(req.body, false);
262
262
+
await updateBook(res.locals.isbn, bookData);
260
263
res.sendStatus(204);
261
264
});
262
265
263
263
-
router.delete("/reset", async (_req, res) => {
264
264
-
await initBooks();
265
265
-
res.sendStatus(204);
266
266
-
});
267
267
-
268
268
-
router.delete("/:id", async (req, res) => {
269
269
-
res.locals.id = req.params.id;
270
270
-
if (!ISBN13.test(req.params.id)) {
271
271
-
throw new BookError(ErrorType.InvalidId);
272
272
-
}
266
266
+
router.delete("/:isbn", getISBN, async (_req, res) => {
273
267
const books = await getBooks();
274
274
-
const book = books.find((b) => b.id == req.params.id);
268
268
+
const book = books.find((b) => b.isbn == res.locals.isbn);
275
269
if (!book) throw new BookError(ErrorType.NotFound);
276
276
-
await removeBook(book.id);
270
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
9
+
"mongodb": "^7.0.0",
9
10
},
10
11
"devDependencies": {
11
12
"@types/bun": "latest",
···
16
17
},
17
18
},
18
19
"packages": {
20
20
+
"@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.4", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-p7X/ytJDIdwUfFL/CLOhKgdfJe1Fa8uw9seJYvdOmnP9JBWGWHW69HkOixXS6Wy9yvGf1MbhcS6lVmrhy4jm2g=="],
21
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
44
+
"@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="],
45
45
+
46
46
+
"@types/whatwg-url": ["@types/whatwg-url@13.0.0", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q=="],
47
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
51
+
52
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
124
+
"memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="],
125
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
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
133
+
134
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
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
151
+
152
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
177
+
178
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
184
+
"tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="],
185
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
195
+
196
196
+
"webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="],
197
197
+
198
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
14
-
"express": "^5.2.1"
14
14
+
"express": "^5.2.1",
15
15
+
"mongodb": "^7.0.0"
15
16
}
16
17
}