diff --git a/.github/workflows/bun-test.yml b/.github/workflows/bun-test.yml index f562cc5a..584275a0 100644 --- a/.github/workflows/bun-test.yml +++ b/.github/workflows/bun-test.yml @@ -7,7 +7,7 @@ on: jobs: test: runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 45 steps: - name: Checkout code @@ -21,6 +21,9 @@ jobs: - name: Install dependencies run: bun install + - name: Install cf-proxy dependencies + run: cd cf-proxy && bun install + - name: Cache setup artifacts id: cache-setup uses: actions/cache@v4 diff --git a/cf-proxy/src/components.ts b/cf-proxy/src/components.ts index 6b25a12a..c3c5c4bb 100644 --- a/cf-proxy/src/components.ts +++ b/cf-proxy/src/components.ts @@ -8,6 +8,7 @@ export interface ComponentCatalogQueryParams { search?: string is_basic?: string is_preferred?: string + is_extended_promotional?: string } export async function queryComponentCatalog( @@ -34,6 +35,7 @@ export async function queryComponentCatalog( subcategory_name: params.subcategory_name, is_basic: params.is_basic, is_preferred: params.is_preferred, + is_extended_promotional: params.is_extended_promotional, limit: "100", }) diff --git a/cf-proxy/src/index.ts b/cf-proxy/src/index.ts index 36b6160c..e340ddcd 100644 --- a/cf-proxy/src/index.ts +++ b/cf-proxy/src/index.ts @@ -367,6 +367,7 @@ async function handleD1Search( package: row.package ?? "", is_basic: Boolean(row.basic), is_preferred: Boolean(row.preferred), + is_extended_promotional: Boolean(row.preferred) && !Boolean(row.basic), description: row.description ?? "", stock: row.stock ?? 0, price: row.price1 ?? extractSmallQuantityPrice(row.price), @@ -491,6 +492,7 @@ async function handleD1ComponentsList( subcategory: row.subcategory ?? "", is_basic: Boolean(row.basic), is_preferred: Boolean(row.preferred), + is_extended_promotional: Boolean(row.preferred) && !Boolean(row.basic), })), } diff --git a/cf-proxy/src/render.ts b/cf-proxy/src/render.ts index 11ce224c..679522bc 100644 --- a/cf-proxy/src/render.ts +++ b/cf-proxy/src/render.ts @@ -147,6 +147,7 @@ const COLUMN_LABELS: Record = { in_stock: "In Stock", is_basic: "Basic", is_preferred: "Preferred", + is_extended_promotional: "Extended Promotional", capacitance_farads: "Capacitance", tolerance_fraction: "Tolerance", voltage_rating: "Voltage", @@ -546,6 +547,9 @@ const renderComponentsFilters = (
+
+ +
` diff --git a/cf-proxy/src/search.ts b/cf-proxy/src/search.ts index f1b60d3c..7613bc1a 100644 --- a/cf-proxy/src/search.ts +++ b/cf-proxy/src/search.ts @@ -9,6 +9,7 @@ export interface SearchQueryParams { limit?: string is_basic?: string is_preferred?: string + is_extended_promotional?: string } interface SearchRow { @@ -110,6 +111,14 @@ export async function searchIndex( conditions.push(sql`search_index.preferred = 1`) } + if ( + params.is_extended_promotional === "true" || + params.is_extended_promotional === "1" + ) { + conditions.push(sql`search_index.preferred = 1`) + conditions.push(sql`search_index.basic = 0`) + } + const raw = params.q?.trim() if (raw) { diff --git a/lib/db/derivedtables/setup-derived-tables.ts b/lib/db/derivedtables/setup-derived-tables.ts index caba7f36..8de602a7 100644 --- a/lib/db/derivedtables/setup-derived-tables.ts +++ b/lib/db/derivedtables/setup-derived-tables.ts @@ -1,5 +1,5 @@ import { sql } from "kysely" -import { getDbClient } from "lib/db/get-db-client" +import { destroyDbClient, getDbClient } from "lib/db/get-db-client" import { accelerometerTableSpec } from "lib/db/derivedtables/accelerometer" import { adcTableSpec } from "lib/db/derivedtables/adc" import { analogMultiplexerTableSpec } from "lib/db/derivedtables/analog_multiplexer" @@ -214,7 +214,7 @@ export const setupDerivedTables = async ({ } } finally { if (shouldDestroy) { - await activeDb.destroy() + await destroyDbClient() } } } diff --git a/lib/db/get-db-client.ts b/lib/db/get-db-client.ts index bbbd6af0..42e749f0 100644 --- a/lib/db/get-db-client.ts +++ b/lib/db/get-db-client.ts @@ -83,6 +83,14 @@ export const getDbClient = () => { return dbClientSingleton } +export const destroyDbClient = async () => { + const db = dbClientSingleton + if (!db) return + + dbClientSingleton = undefined + await db.destroy() +} + export const getBunDatabaseClient = () => { const Database = getDatabaseCtor() return new Database(getResolvedDbPath()) diff --git a/lib/util/is-extended-promotional.ts b/lib/util/is-extended-promotional.ts new file mode 100644 index 00000000..bc0e0cf1 --- /dev/null +++ b/lib/util/is-extended-promotional.ts @@ -0,0 +1,4 @@ +export const isExtendedPromotional = (component: { + basic?: boolean | number | null + preferred?: boolean | number | null +}): boolean => Boolean(component.preferred) && !Boolean(component.basic) diff --git a/routes/api/search.tsx b/routes/api/search.tsx index a95d0da2..d444f429 100644 --- a/routes/api/search.tsx +++ b/routes/api/search.tsx @@ -1,4 +1,5 @@ import { sql } from "kysely" +import { isExtendedPromotional } from "lib/util/is-extended-promotional" import { buildSearchTokenGroups, type SearchTokenGroup, @@ -50,6 +51,7 @@ export default withWinterSpec({ limit: z.string().optional(), is_basic: z.boolean().optional(), is_preferred: z.boolean().optional(), + is_extended_promotional: z.boolean().optional(), }), jsonResponse: z.any(), } as const)(async (req, ctx) => { @@ -72,6 +74,9 @@ export default withWinterSpec({ if (req.query.is_preferred) { query = query.where("preferred", "=", 1) } + if (req.query.is_extended_promotional) { + query = query.where("preferred", "=", 1).where("basic", "=", 0) + } const baseQuery = query let fallbackLikeTokens: string[] = [] @@ -193,12 +198,19 @@ export default withWinterSpec({ package: c.package, is_basic: Boolean(c.basic), is_preferred: Boolean(c.preferred), + is_extended_promotional: isExtendedPromotional(c), description: c.description, stock: c.stock, price: extractSmallQuantityPrice(c.price), })) + const fullComponentsWithExtendedPromotional = fullComponents.map((c) => ({ + ...c, + is_extended_promotional: isExtendedPromotional(c), + })) return ctx.json({ - components: req.query.full ? fullComponents : components, + components: req.query.full + ? fullComponentsWithExtendedPromotional + : components, }) }) diff --git a/routes/components/list.tsx b/routes/components/list.tsx index 640785d1..c544bc10 100644 --- a/routes/components/list.tsx +++ b/routes/components/list.tsx @@ -1,6 +1,7 @@ import { sql } from "kysely" import { Table } from "lib/ui/Table" import { ExpressionBuilder } from "kysely" +import { isExtendedPromotional } from "lib/util/is-extended-promotional" import { buildSearchTokenGroups } from "lib/util/search-token-groups" import { withWinterSpec } from "lib/with-winter-spec" import { z } from "zod" @@ -35,6 +36,7 @@ export default withWinterSpec({ search: z.string().optional(), is_basic: z.boolean().optional(), is_preferred: z.boolean().optional(), + is_extended_promotional: z.boolean().optional(), }), jsonResponse: z.any(), } as const)(async (req, ctx) => { @@ -51,6 +53,7 @@ export default withWinterSpec({ "price", "extra", "basic", + "preferred", ]) .limit(limit) .orderBy("stock", "desc") @@ -70,6 +73,9 @@ export default withWinterSpec({ if (req.query.is_preferred) { query = query.where("preferred", "=", 1) } + if (req.query.is_extended_promotional) { + query = query.where("preferred", "=", 1).where("basic", "=", 0) + } if (req.query.search) { const search = req.query.search @@ -111,14 +117,21 @@ export default withWinterSpec({ package: c.package, is_basic: Boolean(c.basic), is_preferred: Boolean(c.preferred), + is_extended_promotional: isExtendedPromotional(c), description: c.description, stock: c.stock, price: extractSmallQuantityPrice(c.price), })) + const fullComponentsWithExtendedPromotional = fullComponents.map((c) => ({ + ...c, + is_extended_promotional: isExtendedPromotional(c), + })) if (ctx.isApiRequest) { return ctx.json({ - components: req.query.full ? fullComponents : components, + components: req.query.full + ? fullComponentsWithExtendedPromotional + : components, }) } @@ -155,13 +168,28 @@ export default withWinterSpec({ /> +
+ +
{req.query.subcategory_name && (
Filtering by subcategory: {req.query.subcategory_name}
)} - +
, req.query.search ? `${req.query.search} - JLCPCB Component Search` diff --git a/scripts/setup-7z.ts b/scripts/setup-7z.ts index 75f1b7c0..51a94b2e 100644 --- a/scripts/setup-7z.ts +++ b/scripts/setup-7z.ts @@ -7,10 +7,10 @@ const BINARY_NAME = "7zz" // Map of platform-arch combinations to download URLs const BINARY_URLS: Record = { - "linux-x64": "https://7-zip.org/a/7z2408-linux-x64.tar.xz", - "linux-arm64": "https://7-zip.org/a/7z2408-linux-arm64.tar.xz", - "darwin-x64": "https://7-zip.org/a/7z2408-mac.tar.xz", - "darwin-arm64": "https://7-zip.org/a/7z2408-mac.tar.xz", + "linux-x64": "https://7-zip.org/a/7z2601-linux-x64.tar.xz", + "linux-arm64": "https://7-zip.org/a/7z2601-linux-arm64.tar.xz", + "darwin-x64": "https://7-zip.org/a/7z2601-mac.tar.xz", + "darwin-arm64": "https://7-zip.org/a/7z2601-mac.tar.xz", } async function downloadAndExtract7z() { diff --git a/tests/lib/is-extended-promotional.test.ts b/tests/lib/is-extended-promotional.test.ts new file mode 100644 index 00000000..cf1ed29e --- /dev/null +++ b/tests/lib/is-extended-promotional.test.ts @@ -0,0 +1,10 @@ +import { expect, test } from "bun:test" +import { isExtendedPromotional } from "lib/util/is-extended-promotional" + +test("isExtendedPromotional is true for preferred non-basic components", () => { + expect(isExtendedPromotional({ preferred: 1, basic: 0 })).toBe(true) + expect(isExtendedPromotional({ preferred: true, basic: false })).toBe(true) + expect(isExtendedPromotional({ preferred: 1, basic: 1 })).toBe(false) + expect(isExtendedPromotional({ preferred: 0, basic: 0 })).toBe(false) + expect(isExtendedPromotional({ preferred: null, basic: null })).toBe(false) +}) diff --git a/tests/routes/api/search.test.ts b/tests/routes/api/search.test.ts index 90f3794a..c238f9cd 100644 --- a/tests/routes/api/search.test.ts +++ b/tests/routes/api/search.test.ts @@ -107,3 +107,20 @@ test("GET /api/search supports '0402 LED'", async () => { expect(res.data.components.every((c: any) => c.package === "0402")).toBe(true) expect(res.data.components.some((c: any) => c.lcsc === 965793)).toBe(true) }) + +test("GET /api/search exposes and filters extended promotional components", async () => { + const { axios } = await getTestServer() + const res = await axios.get( + "/api/search?limit=50&is_extended_promotional=true", + ) + + expect(res.data).toHaveProperty("components") + expect(Array.isArray(res.data.components)).toBe(true) + expect(res.data.components.length).toBeGreaterThan(0) + + for (const component of res.data.components) { + expect(component).toHaveProperty("is_extended_promotional", true) + expect(component).toHaveProperty("is_basic", false) + expect(component).toHaveProperty("is_preferred", true) + } +}) diff --git a/tests/routes/components/list.test.ts b/tests/routes/components/list.test.ts index 061f1c3b..42b4bd8e 100644 --- a/tests/routes/components/list.test.ts +++ b/tests/routes/components/list.test.ts @@ -1,4 +1,4 @@ -import { test, expect } from "bun:test" +import { expect, test } from "bun:test" import { getTestServer } from "tests/fixtures/get-test-server" test("GET /components/list with json param returns component data", async () => { @@ -7,3 +7,20 @@ test("GET /components/list with json param returns component data", async () => expect(res.data).toHaveProperty("components") expect(Array.isArray(res.data.components)).toBe(true) }) + +test("GET /components/list exposes and filters extended promotional components", async () => { + const { axios } = await getTestServer() + const res = await axios.get( + "/components/list?json=true&is_extended_promotional=true", + ) + + expect(res.data).toHaveProperty("components") + expect(Array.isArray(res.data.components)).toBe(true) + expect(res.data.components.length).toBeGreaterThan(0) + + for (const component of res.data.components) { + expect(component).toHaveProperty("is_extended_promotional", true) + expect(component).toHaveProperty("is_basic", false) + expect(component).toHaveProperty("is_preferred", true) + } +})