source: database/drizzle/queries/components.ts@ b0685a8

main
Last change on this file since b0685a8 was b0685a8, checked in by Tome <gjorgievtome@…>, 6 months ago

Allow adding multiple components per build

  • Property mode set to 100644
File size: 32.8 KB
Line 
1import type { Database } from "../db";
2import {
3 buildComponentsTable,
4 buildsTable, cablesTable, caseMoboFormFactorsTable, casePsFormFactorsTable, caseStorageFormFactorsTable,
5 componentsTable, coolerCPUSocketsTable, coolersTable,
6 CPUTable,
7 GPUTable, memoryCardsTable,
8 memoryTable, motherboardsTable,
9 networkAdaptersTable,
10 networkCardsTable, opticalDrivesTable, pcCasesTable, powerSupplyTable,
11 ratingBuildsTable, soundCardsTable, storageTable
12} from "../schema";
13import {and, asc, desc, eq, ilike, SQL, sql} from "drizzle-orm";
14import {typeConfigMap, ComponentType, requiredFields} from "../util/componentFieldConfig";
15import {AnyPgColumn} from "drizzle-orm/pg-core";
16import {inArray} from "drizzle-orm/sql/expressions/conditions";
17export async function getAllComponents(db: Database, limit?: number, componentType?: string, sort?: string, q?: string) {
18 let queryConditions = [];
19 let sortConditions = [];
20
21 if (q) {
22 queryConditions.push(
23 ilike(componentsTable.name, `%${q}%`)
24 );
25 }
26
27 switch(sort) {
28 case 'price_asc':
29 sortConditions.push(
30 asc(componentsTable.price)
31 );
32 break;
33 case 'price_desc':
34 sortConditions.push(
35 desc(componentsTable.price)
36 );
37 break;
38 default:
39 sortConditions.push(
40 desc(componentsTable.price)
41 );
42 break;
43 }
44
45 if(componentType && componentType.trim() !== 'all') {
46 queryConditions.push(
47 eq(componentsTable.type, componentType.trim().toLowerCase())
48 );
49 }
50
51 const components = await db
52 .select()
53 .from(componentsTable)
54 .where(
55 queryConditions.length > 0 ?
56 and (
57 ...queryConditions
58 )
59 : undefined
60 )
61 .orderBy(
62 ...sortConditions
63 )
64 .limit(limit || 100); // 100 placeholder
65
66 return components;
67}
68
69export async function getComponentDetails(db: Database, componentId: number) {
70 const [component] = await db
71 .select()
72 .from(componentsTable)
73 .where(
74 eq(componentsTable.id, componentId)
75 )
76 .limit(1);
77
78 if(!component) return null;
79
80 const config = typeConfigMap[component.type as ComponentType];
81
82 const [details] = await db
83 .select()
84 .from(config.table)
85 .where(
86 eq(config.table.componentId, componentId)
87 )
88 .limit(1);
89
90 if (component.type === 'case') {
91 details.storageFormFactors = await db.select().from(caseStorageFormFactorsTable).where(eq(caseStorageFormFactorsTable.caseId, componentId));
92 details.psFormFactors = await db.select().from(casePsFormFactorsTable).where(eq(casePsFormFactorsTable.caseId, componentId));
93 details.moboFormFactors = await db.select().from(caseMoboFormFactorsTable).where(eq(caseMoboFormFactorsTable.caseId, componentId));
94 }
95
96 if (component.type === 'cooler') {
97 details.cpuSockets = await db.select().from(coolerCPUSocketsTable).where(eq(coolerCPUSocketsTable.coolerId, componentId));
98 }
99
100 return {
101 ...component,
102 details: details
103 };
104}
105
106export async function getDetailsForNewComponent(componentType: string) {
107 const config = typeConfigMap[componentType as ComponentType];
108
109 return {
110 type: componentType as ComponentType,
111 requiredFields: requiredFields[componentType as ComponentType] ?? [],
112 multiTables: config.multiTables ?? {},
113 };
114}
115
116export async function addNewComponent(db: Database, name: string, brand: string, price: number, imgUrl: string, type: string, specificData: any) {
117 return db.transaction(async (tx) => {
118 const [newComponent] = await tx
119 .insert(componentsTable)
120 .values({
121 name: name,
122 brand: brand,
123 price: price.toFixed(2),
124 imgUrl: imgUrl,
125 type: type
126 })
127 .returning({
128 id: componentsTable.id
129 });
130
131 const componentId = newComponent.id;
132
133 const config = typeConfigMap[type as ComponentType];
134
135 await tx
136 .insert(config.table)
137 .values({
138 componentId: componentId,
139 ...specificData
140 });
141
142 if (type === 'case') {
143 if (specificData.storageFormFactors) {
144 await tx.insert(caseStorageFormFactorsTable).values(
145 specificData.storageFormFactors.map((sf: any) => ({
146 caseId: componentId,
147 formFactor: sf.formFactor,
148 numSlots: sf.numSlots,
149 }))
150 );
151 }
152 if (specificData.psFormFactors) {
153 await tx.insert(casePsFormFactorsTable).values(
154 specificData.psFormFactors.map((pf: any) => ({
155 caseId: componentId,
156 formFactor: pf.formFactor,
157 }))
158 );
159 }
160 if (specificData.moboFormFactors) {
161 await tx.insert(caseMoboFormFactorsTable).values(
162 specificData.moboFormFactors.map((mf: any) => ({
163 caseId: componentId,
164 formFactor: mf.formFactor,
165 }))
166 );
167 }
168 }
169
170 if (type === 'cooler' && specificData.cpuSockets) {
171 await tx.insert(coolerCPUSocketsTable).values(
172 specificData.cpuSockets.map((socket: any) => ({ coolerId: componentId, socket }))
173 );
174 }
175
176 return componentId;
177 });
178}
179
180export async function getBuildComponents(db: Database, buildId: number) {
181 const [build] = await db
182 .select({
183 buildId: buildsTable.id,
184 userId: buildsTable.userId,
185 })
186 .from(buildsTable)
187 .where(
188 eq(buildsTable.id, buildId)
189 )
190 .limit(1);
191
192 if(!build) return null;
193
194 const components = await db
195 .select({
196 id: componentsTable.id,
197 type: componentsTable.type,
198 })
199 .from(buildComponentsTable)
200 .where(
201 eq(buildComponentsTable.buildId, buildId)
202 )
203 .innerJoin(
204 componentsTable,
205 eq(buildComponentsTable.componentId, componentsTable.id)
206 );
207
208 const componentsDetails = [];
209
210 for (const comp of components) {
211 const config = typeConfigMap[comp.type as ComponentType];
212
213 const [details] = await db
214 .select()
215 .from(config.table)
216 .where(
217 eq(config.table.componentId, comp.id)
218 )
219 .limit(1);
220
221 if (comp.type === 'case') {
222 details.storageFormFactors = await db.select().from(caseStorageFormFactorsTable).where(eq(caseStorageFormFactorsTable.caseId, comp.id));
223 details.psFormFactors = await db.select().from(casePsFormFactorsTable).where(eq(casePsFormFactorsTable.caseId, comp.id));
224 details.moboFormFactors = await db.select().from(caseMoboFormFactorsTable).where(eq(caseMoboFormFactorsTable.caseId, comp.id));
225 }
226
227 if (comp.type === 'cooler') {
228 details.cpuSockets = await db.select().from(coolerCPUSocketsTable).where(eq(coolerCPUSocketsTable.coolerId, comp.id));
229 }
230
231 componentsDetails.push({
232 ...comp,
233 details: details
234 });
235 }
236
237 return componentsDetails;
238}
239
240export async function getCompatibleComponents(db: Database, buildId: number, componentType: string, limit?: number, sort?: string) {
241 let sortCondition: any;
242
243 switch(sort) {
244 case 'price_asc':
245 sortCondition = asc(componentsTable.price);
246 break;
247 case 'price_desc':
248 sortCondition = desc(componentsTable.price);
249 break;
250 default:
251 sortCondition = desc(componentsTable.price);
252 break;
253 }
254
255 const existingComponents = await getBuildComponents(db, buildId);
256
257 if(!existingComponents) return null;
258
259
260
261 const existing = {
262 cpu: existingComponents.find(c => c.type === 'cpu'),
263 motherboard: existingComponents.find(c => c.type === 'motherboard'),
264 case: existingComponents.find(c => c.type === 'case'),
265 cooler: existingComponents.find(c => c.type === 'cooler'),
266 psu: existingComponents.find(c => c.type === 'power_supply'),
267 gpu: existingComponents.find(c => c.type === 'gpu'),
268 memory: existingComponents.filter(c => c.type === 'memory'),
269 storage: existingComponents.filter(c => c.type === 'storage'),
270 networkCards: existingComponents.filter(c => c.type === 'network_card'),
271 networkAdapters: existingComponents.filter(c => c.type === 'network_adapter'),
272 soundCards: existingComponents.filter(c => c.type === 'sound_card'),
273 };
274
275 const pciExpressSlotsUsed = [
276 existing.gpu,
277 ...existing.networkCards,
278 ...existing.networkAdapters,
279 ...existing.soundCards
280 ].filter(Boolean).length;
281
282 const existingTDP = existingComponents.reduce((sum, c) => {
283 const tdp = c.details?.tdp ? Number(c.details.tdp) : 0;
284 return sum + tdp;
285 }, 0);
286
287
288 let compatibleComponents: any[] = [];
289
290 switch (componentType) {
291 case 'cpu':
292 compatibleComponents = await getCompatibleCPUs(db, existing, existingTDP, limit, sortCondition);
293 break;
294 case 'motherboard':
295 compatibleComponents = await getCompatibleMotherboards(db, existing, pciExpressSlotsUsed, limit, sortCondition);
296 break;
297 case 'cooler':
298 compatibleComponents = await getCompatibleCoolers(db, existing, limit, sortCondition);
299 break;
300 case 'gpu':
301 compatibleComponents = await getCompatibleGPUs(db, existing, existingTDP, pciExpressSlotsUsed, limit, sortCondition);
302 break;
303 case 'memory':
304 compatibleComponents = await getCompatibleMemory(db, existing, limit, sortCondition);
305 break;
306 case 'storage':
307 compatibleComponents = await getCompatibleStorage(db, existing, limit, sortCondition);
308 break;
309 case 'case':
310 compatibleComponents = await getCompatibleCases(db, existing, limit, sortCondition);
311 break;
312 case 'power_supply':
313 compatibleComponents = await getCompatiblePSUs(db, existing, existingTDP, limit, sortCondition);
314 break;
315 case 'network_card':
316 case 'network_adapter':
317 case 'sound_card':
318 compatibleComponents = await getCompatiblePCIeComponents(db, existing, componentType, pciExpressSlotsUsed, limit, sortCondition);
319 break;
320 default:
321 compatibleComponents = [];
322 }
323
324 if(compatibleComponents.length === 0) return null;
325
326 return compatibleComponents;
327}
328
329
330// Helper functions for checking component-specific compatibility
331async function getCompatibleCPUs(db: Database, existing: any, existingTDP?: number, limit?: number, sortCondition?: any) {
332 const conditions = [eq(componentsTable.type, 'cpu')];
333
334 if (existing.motherboard) {
335 conditions.push(
336 eq(CPUTable.socket, existing.motherboard.details.socket)
337 );
338 }
339
340 if (existing.cooler) {
341 conditions.push(
342 sql`${CPUTable.tdp} <= ${existing.cooler.details.maxTdpSupported}`
343 );
344 }
345
346 const cpus = await db
347 .select({
348 id: componentsTable.id,
349 name: componentsTable.name,
350 brand: componentsTable.brand,
351 price: componentsTable.price,
352 imgUrl: componentsTable.imgUrl,
353 type: componentsTable.type,
354 socket: CPUTable.socket,
355 cores: CPUTable.cores,
356 threads: CPUTable.threads,
357 baseClock: CPUTable.baseClock,
358 boostClock: CPUTable.boostClock,
359 tdp: CPUTable.tdp,
360 })
361 .from(componentsTable)
362 .innerJoin(
363 CPUTable,
364 eq(componentsTable.id, CPUTable.componentId)
365 )
366 .where(
367 and(
368 ...conditions
369 )
370 )
371 .orderBy(
372 sortCondition
373 )
374 .limit(limit || 100);
375
376 let filteredCPUs = cpus;
377
378 if (existing.cooler?.details?.cpuSockets) {
379 const coolerSockets = existing.cooler.details.cpuSockets.map((s: any) => s.socket);
380 filteredCPUs = filteredCPUs.filter(cpu => coolerSockets.includes(cpu.socket));
381 }
382
383 if(existing.psu?.details?.wattage && existingTDP) {
384 const psuWattage = Number(existing.psu.details.wattage) || 0;
385
386 if (psuWattage > 0) {
387 const budget = psuWattage / 1.2;
388
389 const currentCpuTdp = Number(existing.cpu?.details?.tdp) || 0;
390 const baseWithoutCpu = existingTDP - currentCpuTdp;
391
392 filteredCPUs = filteredCPUs.filter(cpu => {
393 const cpuTdp = Number(cpu.tdp) || 0;
394 return (baseWithoutCpu + cpuTdp) <= budget;
395 });
396 }
397 }
398
399 return filteredCPUs;
400}
401
402async function getCompatibleMotherboards(db: Database, existing: any, pciExpressSlotsUsed: number, limit?: number, sortCondition?: any) {
403 const conditions = [eq(componentsTable.type, 'motherboard')];
404
405 if (existing.cpu) {
406 conditions.push(
407 eq(motherboardsTable.socket, existing.cpu.details.socket)
408 );
409 }
410
411 if (existing.memory.length > 0) {
412 const ramType = existing.memory[0].details.type;
413 conditions.push(
414 eq(motherboardsTable.ramType, ramType)
415 );
416 }
417
418 if (pciExpressSlotsUsed > 0) {
419 conditions.push(
420 sql`${motherboardsTable.pciExpressSlots} >= ${pciExpressSlotsUsed}`
421 );
422 }
423
424 const motherboards = await db
425 .select({
426 id: componentsTable.id,
427 name: componentsTable.name,
428 brand: componentsTable.brand,
429 price: componentsTable.price,
430 imgUrl: componentsTable.imgUrl,
431 type: componentsTable.type,
432 socket: motherboardsTable.socket,
433 chipset: motherboardsTable.chipset,
434 formFactor: motherboardsTable.formFactor,
435 ramType: motherboardsTable.ramType,
436 numRamSlots: motherboardsTable.numRamSlots,
437 maxRamCapacity: motherboardsTable.maxRamCapacity,
438 pciExpressSlots: motherboardsTable.pciExpressSlots,
439 })
440 .from(componentsTable)
441 .innerJoin(
442 motherboardsTable,
443 eq(componentsTable.id, motherboardsTable.componentId)
444 )
445 .where(
446 and(
447 ...conditions
448 )
449 )
450 .orderBy(
451 sortCondition
452 )
453 .limit(limit || 100);
454
455 const compatibleMotherboards = motherboards.filter(mobo => {
456 if (existing.memory.length > 0) {
457 const totalModules = existing.memory.reduce((sum: number, m: any) =>
458 sum + Number(m.details.modules), 0
459 );
460 const totalCapacity = existing.memory.reduce((sum: number, m: any) =>
461 sum + Number(m.details.capacity), 0
462 );
463
464 if (totalModules > Number(mobo.numRamSlots)) return false;
465 if (totalCapacity > Number(mobo.maxRamCapacity)) return false;
466 }
467
468 return true;
469 });
470
471 if (existing.case?.details?.moboFormFactors) {
472 const caseFormFactors = existing.case.details.moboFormFactors.map((f: any) => f.formFactor);
473 return compatibleMotherboards.filter(mobo => caseFormFactors.includes(mobo.formFactor));
474 }
475
476 return compatibleMotherboards;
477}
478
479async function getCompatibleCoolers(db: Database, existing: any, limit?: number, sortCondition?: any) {
480 const conditions = [eq(componentsTable.type, 'cooler')];
481
482 if (existing.cpu) {
483 conditions.push(
484 sql`${coolersTable.maxTdpSupported} >= ${existing.cpu.details.tdp}`
485 );
486 }
487
488 if (existing.case) {
489 conditions.push(
490 sql`${coolersTable.height} <= ${existing.case.details.coolerMaxHeight}`
491 );
492 }
493
494 const coolers = await db
495 .select({
496 id: componentsTable.id,
497 name: componentsTable.name,
498 brand: componentsTable.brand,
499 price: componentsTable.price,
500 imgUrl: componentsTable.imgUrl,
501 type: componentsTable.type,
502 coolerType: coolersTable.type,
503 height: coolersTable.height,
504 maxTdpSupported: coolersTable.maxTdpSupported,
505 })
506 .from(componentsTable)
507 .innerJoin(
508 coolersTable,
509 eq(componentsTable.id, coolersTable.componentId)
510 )
511 .where(
512 and(
513 ...conditions
514 )
515 )
516 .orderBy(
517 sortCondition
518 )
519 .limit(limit || 100);
520
521
522 if(existing.cpu && coolers.length > 0) {
523 const coolerIds = coolers.map(c => c.id);
524
525 const allSockets = await db
526 .select({
527 coolerId: coolerCPUSocketsTable.coolerId,
528 socket: coolerCPUSocketsTable.socket
529 })
530 .from(coolerCPUSocketsTable)
531 .where(
532 inArray(coolerCPUSocketsTable.coolerId, coolerIds)
533 );
534 const socketsByCooler = allSockets.reduce((acc, { coolerId, socket }) => {
535 if (!acc[coolerId]) acc[coolerId] = [];
536 acc[coolerId].push(socket);
537 return acc;
538 }, {} as Record<number, string[]>);
539
540 return coolers.filter(cooler => {
541 const sockets = socketsByCooler[cooler.id] || [];
542 return sockets.includes(existing.cpu.details.socket);
543 });
544 }
545
546 return coolers;
547}
548
549async function getCompatibleGPUs(db: Database, existing: any, existingTDP: number, pciExpressSlotsUsed: number, limit?: number, sortCondition?: any) {
550 const conditions = [eq(componentsTable.type, 'gpu')];
551
552 if (existing.case) {
553 conditions.push(
554 sql`${GPUTable.length} <= ${existing.case.details.gpuMaxLength}`
555 );
556 }
557
558 const gpus = await db
559 .select({
560 id: componentsTable.id,
561 name: componentsTable.name,
562 brand: componentsTable.brand,
563 price: componentsTable.price,
564 imgUrl: componentsTable.imgUrl,
565 type: componentsTable.type,
566 vram: GPUTable.vram,
567 tdp: GPUTable.tdp,
568 baseClock: GPUTable.baseClock,
569 boostClock: GPUTable.boostClock,
570 chipset: GPUTable.chipset,
571 length: GPUTable.length,
572 })
573 .from(componentsTable)
574 .innerJoin(
575 GPUTable,
576 eq(componentsTable.id, GPUTable.componentId)
577 )
578 .where(
579 and(
580 ...conditions
581 )
582 )
583 .orderBy(
584 sortCondition
585 )
586 .limit(limit || 100);
587
588 let filteredGPUs = gpus;
589
590 if (existing.motherboard) {
591 const availableSlots = Number(existing.motherboard.details.pciExpressSlots);
592 filteredGPUs = gpus.filter(() => (pciExpressSlotsUsed + 1) <= availableSlots);
593 }
594
595 if(existing.psu?.details?.wattage && existingTDP) {
596 const psuWattage = Number(existing.psu.details.wattage) || 0;
597
598 if (psuWattage > 0) {
599 const budget = psuWattage / 1.2;
600
601 const currentGpuTdp = Number(existing.cpu?.details?.tdp) || 0;
602 const baseWithoutGpu = existingTDP - currentGpuTdp;
603
604 filteredGPUs = filteredGPUs.filter(cpu => {
605 const gpuTdp = Number(cpu.tdp) || 0;
606 return (baseWithoutGpu + gpuTdp) <= budget;
607 });
608 }
609 }
610
611 return filteredGPUs;
612}
613
614async function getCompatibleMemory(db: Database, existing: any, limit?: number, sortCondition?: any) {
615 const conditions = [eq(componentsTable.type, 'memory')];
616
617 if (existing.motherboard) {
618 conditions.push(
619 eq(memoryTable.type, existing.motherboard.details.ramType)
620 );
621 }
622
623 const memory = await db
624 .select({
625 id: componentsTable.id,
626 name: componentsTable.name,
627 brand: componentsTable.brand,
628 price: componentsTable.price,
629 imgUrl: componentsTable.imgUrl,
630 type: componentsTable.type,
631 memoryType: memoryTable.type,
632 speed: memoryTable.speed,
633 capacity: memoryTable.capacity,
634 modules: memoryTable.modules,
635 })
636 .from(componentsTable)
637 .innerJoin(
638 memoryTable,
639 eq(componentsTable.id, memoryTable.componentId)
640 )
641 .where(
642 and(
643 ...conditions
644 )
645 )
646 .orderBy(
647 sortCondition
648 )
649 .limit(limit || 100);
650
651 if (existing.motherboard) {
652 const existingModules = existing.memory.reduce((sum: number, m: any) =>
653 sum + Number(m.details.modules), 0
654 );
655 const existingCapacity = existing.memory.reduce((sum: number, m: any) =>
656 sum + Number(m.details.capacity), 0
657 );
658 const maxSlots = Number(existing.motherboard.details.numRamSlots);
659 const maxCapacity = Number(existing.motherboard.details.maxRamCapacity);
660
661 return memory.filter(mem => {
662 const newModules = existingModules + Number(mem.modules);
663 const newCapacity = existingCapacity + Number(mem.capacity);
664 return newModules <= maxSlots && newCapacity <= maxCapacity;
665 });
666 }
667
668 return memory;
669}
670
671async function getCompatibleStorage(db: Database, existing: any, limit?: number, sortCondition?: any) {
672 const storage = await db
673 .select({
674 id: componentsTable.id,
675 name: componentsTable.name,
676 brand: componentsTable.brand,
677 price: componentsTable.price,
678 imgUrl: componentsTable.imgUrl,
679 type: componentsTable.type,
680 storageType: storageTable.type,
681 capacity: storageTable.capacity,
682 formFactor: storageTable.formFactor,
683 })
684 .from(componentsTable)
685 .innerJoin(
686 storageTable,
687 eq(componentsTable.id, storageTable.componentId
688 )
689 )
690 .where(
691 eq(componentsTable.type, 'storage')
692 )
693 .orderBy(
694 sortCondition
695 )
696 .limit(limit || 100);
697
698 if (existing.case?.details?.storageFormFactors) {
699 return storage.filter(stor => {
700 const caseStorageFormFactor = existing.case.details.storageFormFactors.find(
701 (sf: any) => sf.formFactor === stor.formFactor
702 );
703
704 if (!caseStorageFormFactor) return false;
705
706 const usedSlots = existing.storage.filter(
707 (s: any) => s.details.formFactor === stor.formFactor
708 ).length;
709
710 return usedSlots < Number(caseStorageFormFactor.numSlots);
711 });
712 }
713
714 return storage;
715}
716
717async function getCompatibleCases(db: Database, existing: any, limit?: number, sortCondition?: any) {
718 const conditions = [eq(componentsTable.type, 'case')];
719
720 if (existing.gpu) {
721 conditions.push(
722 sql`${pcCasesTable.gpuMaxLength} >= ${existing.gpu.details.length}`
723 );
724 }
725
726 if (existing.cooler) {
727 conditions.push(
728 sql`${pcCasesTable.coolerMaxHeight} >= ${existing.cooler.details.height}`
729 );
730 }
731
732 const cases = await db
733 .select({
734 id: componentsTable.id,
735 name: componentsTable.name,
736 brand: componentsTable.brand,
737 price: componentsTable.price,
738 imgUrl: componentsTable.imgUrl,
739 type: componentsTable.type,
740 coolerMaxHeight: pcCasesTable.coolerMaxHeight,
741 gpuMaxLength: pcCasesTable.gpuMaxLength,
742 })
743 .from(componentsTable)
744 .innerJoin(
745 pcCasesTable,
746 eq(componentsTable.id, pcCasesTable.componentId)
747 )
748 .where(
749 and(
750 ...conditions)
751 )
752 .orderBy(
753 sortCondition
754 )
755 .limit(limit || 100);
756
757 if(cases.length === 0) return [];
758
759 const caseIds = cases.map(c => c.id);
760
761 const [allStorageFF, allPsFF, allMoboFF] = await Promise.all([
762 db.select().from(caseStorageFormFactorsTable)
763 .where(
764 inArray(caseStorageFormFactorsTable.caseId, caseIds)
765 ),
766 db.select().from(casePsFormFactorsTable)
767 .where(
768 inArray(casePsFormFactorsTable.caseId, caseIds)
769 ),
770 db.select().from(caseMoboFormFactorsTable)
771 .where(
772 inArray(caseMoboFormFactorsTable.caseId, caseIds)
773 )
774 ]);
775
776 const storageByCase = allStorageFF.reduce((acc, ff) => {
777 if (!acc[ff.caseId]) acc[ff.caseId] = [];
778 acc[ff.caseId].push(ff);
779 return acc;
780 }, {} as Record<number, any[]>);
781
782 const psByCase = allPsFF.reduce((acc, ff) => {
783 if (!acc[ff.caseId]) acc[ff.caseId] = [];
784 acc[ff.caseId].push(ff);
785 return acc;
786 }, {} as Record<number, any[]>);
787
788 const moboByCase = allMoboFF.reduce((acc, ff) => {
789 if (!acc[ff.caseId]) acc[ff.caseId] = [];
790 acc[ff.caseId].push(ff);
791 return acc;
792 }, {} as Record<number, any[]>);
793
794 const casesWithDetails = cases.map(pcCase => ({
795 ...pcCase,
796 storageFormFactors: storageByCase[pcCase.id] || [],
797 psFormFactors: psByCase[pcCase.id] || [],
798 moboFormFactors: moboByCase[pcCase.id] || []
799 }));
800
801 let filteredCases = casesWithDetails;
802
803 if (existing.motherboard) {
804 filteredCases = filteredCases.filter(pcCase =>
805 pcCase.moboFormFactors.some((f: any) => f.formFactor === existing.motherboard.details.formFactor)
806 );
807 }
808
809 if (existing.psu) {
810 filteredCases = filteredCases.filter(pcCase =>
811 pcCase.psFormFactors.some((f: any) => f.formFactor === existing.psu.details.formFactor)
812 );
813 }
814
815 if (existing.storage.length > 0) {
816 filteredCases = filteredCases.filter(pcCase => {
817 return existing.storage.every((stor: any) => {
818 const storageFF = pcCase.storageFormFactors.find(
819 (sf: any) => sf.formFactor === stor.details.formFactor
820 );
821 if (!storageFF) return false;
822
823 const usedSlots = existing.storage.filter(
824 (s: any) => s.details.formFactor === stor.details.formFactor
825 ).length;
826
827 return usedSlots <= Number(storageFF.numSlots);
828 });
829 });
830 }
831
832 return filteredCases;
833}
834
835async function getCompatiblePSUs(db: Database, existing: any, existingTDP: number, limit?: number, sortCondition?: any) {
836 const conditions = [eq(componentsTable.type, 'power_supply')];
837
838 const minWattage = existingTDP * 1.2;
839 conditions.push(
840 sql`${powerSupplyTable.wattage} >= ${minWattage}`
841 );
842
843 const psus = await db
844 .select({
845 id: componentsTable.id,
846 name: componentsTable.name,
847 brand: componentsTable.brand,
848 price: componentsTable.price,
849 imgUrl: componentsTable.imgUrl,
850 type: componentsTable.type,
851 psuType: powerSupplyTable.type,
852 wattage: powerSupplyTable.wattage,
853 formFactor: powerSupplyTable.formFactor,
854 })
855 .from(componentsTable)
856 .innerJoin(
857 powerSupplyTable,
858 eq(componentsTable.id, powerSupplyTable.componentId)
859 )
860 .where(
861 and(
862 ...conditions
863 )
864 )
865 .orderBy(
866 sortCondition
867 )
868 .limit(limit || 100);
869
870 if (existing.case?.details?.psFormFactors) {
871 const casePSFormFactors = existing.case.details.psFormFactors.map((f: any) => f.formFactor);
872 return psus.filter(psu => casePSFormFactors.includes(psu.formFactor));
873 }
874
875 return psus;
876}
877
878async function getCompatiblePCIeComponents(db: Database, existing: any, componentType: string, pciExpressSlotsUsed: number, limit?: number, sortCondition?: any) {
879 let table: any;
880 let selectFields: any = {
881 id: componentsTable.id,
882 name: componentsTable.name,
883 brand: componentsTable.brand,
884 price: componentsTable.price,
885 imgUrl: componentsTable.imgUrl,
886 type: componentsTable.type,
887 };
888
889 switch (componentType) {
890 case 'network_card':
891 table = networkCardsTable;
892 selectFields = {
893 ...selectFields,
894 numPorts: networkCardsTable.numPorts,
895 speed: networkCardsTable.speed,
896 interface: networkCardsTable.interface,
897 };
898 break;
899 case 'network_adapter':
900 table = networkAdaptersTable;
901 selectFields = {
902 ...selectFields,
903 wifiVersion: networkAdaptersTable.wifiVersion,
904 interface: networkAdaptersTable.interface,
905 numAntennas: networkAdaptersTable.numAntennas,
906 };
907 break;
908 case 'sound_card':
909 table = soundCardsTable;
910 selectFields = {
911 ...selectFields,
912 sampleRate: soundCardsTable.sampleRate,
913 bitDepth: soundCardsTable.bitDepth,
914 chipset: soundCardsTable.chipset,
915 interface: soundCardsTable.interface,
916 channel: soundCardsTable.channel,
917 };
918 break;
919 default:
920 return [];
921 }
922
923 const components = await db
924 .select(selectFields)
925 .from(componentsTable)
926 .innerJoin(
927 table,
928 eq(componentsTable.id, table.componentId)
929 )
930 .where(
931 eq(componentsTable.type, componentType)
932 )
933 .orderBy(
934 sortCondition
935 )
936 .limit(limit || 100);
937
938 if (existing.motherboard) {
939 const availableSlots = Number(existing.motherboard.details.pciExpressSlots);
940 return components.filter(() => (pciExpressSlotsUsed + 1) <= availableSlots);
941 }
942
943 return components;
944}
945
946export async function addComponentToBuild(db: Database, userId: number, buildId: number, componentId: number) {
947 return db.transaction(async (tx) => {
948 const [build] = await tx
949 .select()
950 .from(buildsTable)
951 .where(
952 and(
953 eq(buildsTable.id, buildId),
954 eq(buildsTable.userId, userId)
955 )
956 )
957 .limit(1);
958
959 if(!build || build.isApproved) return null;
960
961 await tx
962 .insert(buildComponentsTable)
963 .values({
964 buildId,
965 componentId
966 });
967
968 const buildComponents = await tx
969 .select({
970 price: componentsTable.price,
971 })
972 .from(buildComponentsTable)
973 .innerJoin(
974 componentsTable,
975 eq(buildComponentsTable.componentId, componentsTable.id)
976 )
977 .where(
978 eq(buildComponentsTable.buildId, buildId)
979 );
980
981 const totalPrice = buildComponents.reduce((sum, c) => sum + Number(c.price), 0);
982
983 await tx
984 .update(buildsTable)
985 .set({
986 totalPrice: totalPrice.toFixed(2)
987 })
988 .where(
989 eq(buildsTable.id, buildId)
990 );
991
992 return buildId;
993 })
994}
995
996export async function removeComponentFromBuild(db: Database, userId: number, buildId: number, componentId: number) {
997 return db.transaction(async (tx) => {
998 const [build] = await tx
999 .select()
1000 .from(buildsTable)
1001 .where(
1002 and(
1003 eq(buildsTable.id, buildId),
1004 eq(buildsTable.userId, userId)
1005 )
1006 )
1007 .limit(1);
1008
1009 if(!build || build.isApproved) return null;
1010
1011 const result = await tx
1012 .delete(buildComponentsTable)
1013 .where(
1014 and(
1015 eq(buildComponentsTable.buildId, buildId),
1016 eq(buildComponentsTable.componentId, componentId)
1017 )
1018 );
1019
1020 if(result.rowCount === 0) return null;
1021
1022 const buildComponents = await tx
1023 .select({
1024 price: componentsTable.price,
1025 })
1026 .from(buildComponentsTable)
1027 .innerJoin(
1028 componentsTable,
1029 eq(buildComponentsTable.componentId, componentsTable.id)
1030 )
1031 .where(
1032 eq(buildComponentsTable.buildId, buildId)
1033 );
1034
1035 const totalPrice = buildComponents.reduce((sum, c) => sum + Number(c.price), 0);
1036
1037 await tx
1038 .update(buildsTable)
1039 .set({
1040 totalPrice: totalPrice.toFixed(2)
1041 })
1042 .where(
1043 eq(buildsTable.id, buildId)
1044 );
1045
1046 return result;
1047 })
1048}
Note: See TracBrowser for help on using the repository browser.