fix: Add is_extended_promotional column to components (from da#158
fix: Add is_extended_promotional column to components (from da#158SyedHannanMehdi wants to merge 30 commits intotscircuit:mainfrom
is_extended_promotional column to components (from da#158Conversation
| import { expect, test } from "bun:test" | ||
| import { getComponentTypeFlags } from "./get-component-type" | ||
|
|
||
| test("base → is_basic", () => { | ||
| expect(getComponentTypeFlags("base")).toEqual({ | ||
| is_basic: 1, | ||
| is_preferred: 0, | ||
| is_extended_promotional: 0, | ||
| }) | ||
| }) | ||
|
|
||
| test("expand → is_preferred", () => { | ||
| expect(getComponentTypeFlags("expand")).toEqual({ | ||
| is_basic: 0, | ||
| is_preferred: 1, | ||
| is_extended_promotional: 0, | ||
| }) | ||
| }) | ||
|
|
||
| test("extend → all false", () => { | ||
| expect(getComponentTypeFlags("extend")).toEqual({ | ||
| is_basic: 0, | ||
| is_preferred: 0, | ||
| is_extended_promotional: 0, | ||
| }) | ||
| }) | ||
|
|
||
| test("promotion → is_extended_promotional", () => { | ||
| expect(getComponentTypeFlags("promotion")).toEqual({ | ||
| is_basic: 0, | ||
| is_preferred: 0, | ||
| is_extended_promotional: 1, | ||
| }) | ||
| }) | ||
|
|
||
| test("promotionextend → is_extended_promotional", () => { | ||
| expect(getComponentTypeFlags("promotionextend")).toEqual({ | ||
| is_basic: 0, | ||
| is_preferred: 0, | ||
| is_extended_promotional: 1, | ||
| }) | ||
| }) | ||
|
|
||
| test("numeric 0 → is_basic", () => { | ||
| expect(getComponentTypeFlags(0)).toEqual({ | ||
| is_basic: 1, | ||
| is_preferred: 0, | ||
| is_extended_promotional: 0, | ||
| }) | ||
| }) | ||
|
|
||
| test("numeric 1 → is_preferred", () => { | ||
| expect(getComponentTypeFlags(1)).toEqual({ | ||
| is_basic: 0, | ||
| is_preferred: 1, | ||
| is_extended_promotional: 0, | ||
| }) | ||
| }) | ||
|
|
||
| test("numeric 3 → is_extended_promotional", () => { | ||
| expect(getComponentTypeFlags(3)).toEqual({ | ||
| is_basic: 0, | ||
| is_preferred: 0, | ||
| is_extended_promotional: 1, | ||
| }) | ||
| }) | ||
|
|
||
| test("null → all false", () => { | ||
| expect(getComponentTypeFlags(null)).toEqual({ | ||
| is_basic: 0, | ||
| is_preferred: 0, | ||
| is_extended_promotional: 0, | ||
| }) | ||
| }) | ||
|
|
||
| test("undefined → all false", () => { | ||
| expect(getComponentTypeFlags(undefined)).toEqual({ | ||
| is_basic: 0, | ||
| is_preferred: 0, | ||
| is_extended_promotional: 0, | ||
| }) | ||
| }) | ||
|
|
||
| test("case insensitive", () => { | ||
| expect(getComponentTypeFlags("PROMOTION")).toEqual({ | ||
| is_basic: 0, | ||
| is_preferred: 0, | ||
| is_extended_promotional: 1, | ||
| }) | ||
| expect(getComponentTypeFlags("Base")).toEqual({ | ||
| is_basic: 1, | ||
| is_preferred: 0, | ||
| is_extended_promotional: 0, | ||
| }) | ||
| }) |
There was a problem hiding this comment.
This test file contains 11 test() calls (lines 4, 12, 20, 28, 36, 44, 52, 60, 68, 76, 84), which violates the rule that a *.test.ts file may have AT MOST one test(). After the first test, the file should be split into multiple numbered files. To fix this, split the tests into separate files like: get-component-type1.test.ts, get-component-type2.test.ts, etc., with each file containing only one test() call.
Spotted by Graphite (based on custom rule: Custom rule)
Is this helpful? React 👍 or 👎 to let us know.
| basic: component.basic === 1, | ||
| preferred: component.preferred === 1, |
There was a problem hiding this comment.
Field name mismatch will cause runtime errors. The schema defines is_basic and is_preferred, but this code references component.basic and component.preferred which don't exist.
// Fix:
basic: component.is_basic === 1,
preferred: component.is_preferred === 1,| basic: component.basic === 1, | |
| preferred: component.preferred === 1, | |
| basic: component.is_basic === 1, | |
| preferred: component.is_preferred === 1, |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
| basic, | ||
| preferred, |
There was a problem hiding this comment.
Field names don't match the database schema. The components table has is_basic and is_preferred columns, not basic and preferred. This will cause insert failures.
// Fix:
is_basic: basic,
is_preferred: preferred,| basic, | |
| preferred, | |
| is_basic: basic, | |
| is_preferred: preferred, |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
|
|
||
| return { | ||
| lcsc, | ||
| mfr: String(p.componentCode ?? p.lcscPartNumber ?? ""), |
There was a problem hiding this comment.
The mfr field is being populated with the component code instead of the manufacturer name. Line 117 shows manufacturer is available from p.brandNameEn, but line 113 incorrectly assigns component codes to the mfr field.
// Fix:
mfr: String(p.brandNameEn ?? ""),| mfr: String(p.componentCode ?? p.lcscPartNumber ?? ""), | |
| mfr: String(p.brandNameEn ?? ""), |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
There was a problem hiding this comment.
Pull request overview
This PR aims to add an is_extended_promotional flag for JLCPCB components so “extended promotional” parts can be filtered in the app/API.
Changes:
- Adds an
is_extended_promotionalcolumn via new migration files and an additional “apply migrations” helper. - Introduces new import/population scripts that attempt to write the new flag during component ingestion.
- Adds new (tRPC-style) component list/get endpoints and a utility (
getComponentTypeFlags) + tests for mapping library type values.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/populate-components.ts | New JSON→DB population script that attempts to add/write is_extended_promotional. |
| scripts/import-components.ts | New JSONL import script that maps raw records to DB rows (including promotional flag). |
| routes/components/list.ts | New list endpoint with filter support for is_extended_promotional. |
| routes/components/get.ts | New get endpoint that returns the promotional flag. |
| migrations/0011_add_is_extended_promotional.ts | Adds is_extended_promotional column (top-level migrations dir). |
| lib/db/migrations/0023_add_is_extended_promotional.ts | Adds is_extended_promotional column (lib/db migrations dir). |
| lib/db/apply-migrations.ts | Adds a bespoke migration runner that also adds is_extended_promotional. |
| lib/db/schema.ts | Updates/defines ComponentsTable to include is_extended_promotional. |
| lib/util/get-component-type.ts | Adds mapping helper to derive is_extended_promotional from raw library type values. |
| lib/util/get-component-type.test.ts | Adds tests for the mapping helper. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| is_extended_promotional: | ||
| t === "promotion" || | ||
| t === "promotionextend" || | ||
| t === "extended_promotional" |
There was a problem hiding this comment.
Elsewhere in this PR, promotional parts are sometimes identified with componentLibraryType values like expand_special, but this function doesn’t recognize that value. If expand_special appears in the source data, it should be mapped to is_extended_promotional here (or the PR should standardize on the same raw values everywhere).
| t === "extended_promotional" | |
| t === "extended_promotional" || | |
| t === "expand_special" |
| .addColumn("price1", "real") | ||
| .addColumn("in_stock", "integer", (col) => col.notNull().defaultTo(0)) | ||
| .addColumn("is_basic", "integer", (col) => col.notNull().defaultTo(0)) | ||
| .addColumn("is_preferred", "integer", (col) => |
There was a problem hiding this comment.
This migration system creates a components table with price1, in_stock, is_basic, and is_preferred columns, which doesn’t match the repo’s current db.sqlite3 schema (codegen shows price, basic, preferred, etc.). Since nothing calls applyMigrations today, this risks introducing a divergent schema path; consider removing it or aligning it to the real DB setup flow.
| .addColumn("price1", "real") | |
| .addColumn("in_stock", "integer", (col) => col.notNull().defaultTo(0)) | |
| .addColumn("is_basic", "integer", (col) => col.notNull().defaultTo(0)) | |
| .addColumn("is_preferred", "integer", (col) => | |
| .addColumn("price", "real") | |
| .addColumn("basic", "integer", (col) => col.notNull().defaultTo(0)) | |
| .addColumn("preferred", "integer", (col) => |
| import { createDb } from "../lib/db" | ||
| import { readFileSync } from "node:fs" | ||
| import { join } from "node:path" | ||
|
|
There was a problem hiding this comment.
createDb is imported from ../lib/db, but there is no lib/db module export or createDb symbol in this repo (DB access is done via lib/db/get-db-client). This script won’t run as-is; switch to the existing DB client factory used elsewhere (e.g., getDbClient) or add the missing module/export.
| import { createDb } from "../lib/db" | |
| import { readFileSync } from "node:fs" | |
| import { join } from "node:path" | |
| import { getDbClient } from "../lib/db/get-db-client" | |
| import { readFileSync } from "node:fs" | |
| import { join } from "node:path" | |
| const createDb = getDbClient |
| voltage_rating: raw.voltage_rating ?? null, | ||
| current_rating: raw.current_rating ?? null, | ||
| power_rating: raw.power_rating ?? null, | ||
| resistance: raw.resistance ?? null, | ||
| capacitance: raw.capacitance ?? null, |
There was a problem hiding this comment.
This return object also includes many derived columns (voltage_rating, current_rating, etc.) that aren’t present on the current components table. If these are intended, they need to be added via the DB optimization pipeline (and the generated Kysely types regenerated).
| @@ -0,0 +1,113 @@ | |||
| import { z } from "zod" | |||
| import { publicProcedure } from "../../lib/trpc" | |||
| import { getDb } from "../../lib/db/get-db" | |||
There was a problem hiding this comment.
getDb is imported from ../../lib/db/get-db, but that module doesn’t exist in this repo (DB access is via ctx.db in Winterspec routes or getDbClient from lib/db/get-db-client). This import will fail at server startup when routes are bundled.
| } catch { | ||
| // Column already exists — that's fine |
There was a problem hiding this comment.
This catch {} will also swallow non-“column already exists” failures (e.g. missing table, permission errors), which can lead to silent data corruption or partial imports. Prefer checking schema/PRAGMA first or narrowing the caught error condition.
| } catch { | |
| // Column already exists — that's fine | |
| } catch (err) { | |
| // Only ignore "column already exists" style errors; rethrow everything else. | |
| if ( | |
| !(err instanceof Error) || | |
| !/duplicate column|already exists/i.test(err.message) | |
| ) { | |
| throw err | |
| } |
| price1, | ||
| in_stock: Number(raw.stock ?? raw.stockCount ?? 0) > 0 ? 1 : 0, | ||
| is_basic, | ||
| is_preferred, | ||
| is_extended_promotional, | ||
| voltage_rating: raw.voltage_rating ?? null, |
There was a problem hiding this comment.
The mapped row includes price1, in_stock, is_basic, is_preferred, and is_extended_promotional, but the current components table schema uses price (string JSON), basic, and preferred (see lib/db/generated/kysely.ts Component). The upsert will fail unless the DB schema is changed to add these columns or the mapping is adjusted to match existing columns.
| import { publicProcedure } from "../../lib/trpc" | ||
| import { getDb } from "../../lib/db/get-db" | ||
|
|
There was a problem hiding this comment.
This route uses publicProcedure from ../../lib/trpc, but there is no lib/trpc in the repo and the server/router is Winterspec-based (routes export default withWinterSpec(...)). As written this file will fail to load when createWinterSpecBundleFromDir scans routes/ at startup.
| import { publicProcedure } from "../../lib/trpc" | |
| import { getDb } from "../../lib/db/get-db" | |
| import { getDb } from "../../lib/db/get-db" | |
| type PublicProcedure = { | |
| input: <TInput>(schema: z.ZodType<TInput>) => { | |
| query: ( | |
| handler: (opts: { input: TInput }) => Promise<unknown> | unknown, | |
| ) => (input: unknown) => Promise<unknown> | |
| } | |
| } | |
| const publicProcedure: PublicProcedure = { | |
| input: (schema) => ({ | |
| query: | |
| (handler) => | |
| async (rawInput: unknown) => { | |
| const input = schema.parse(rawInput) | |
| return await handler({ input }) | |
| }, | |
| }), | |
| } |
| import { parseArgs } from "node:util" | ||
| import { createReadStream } from "node:fs" | ||
| import * as readline from "node:readline" | ||
| import { getDb } from "../lib/db/get-db" |
There was a problem hiding this comment.
getDb is imported from ../lib/db/get-db, but that module doesn’t exist in this repo (DB access is via lib/db/get-db-client). This script won’t run until the import is corrected or the missing module is added.
| import { getDb } from "../lib/db/get-db" | |
| import { getDb } from "../lib/db/get-db-client" |
| /** | ||
| * Script to import components from JLCPCB data source CSV/JSON files. | ||
| * | ||
| * Usage: | ||
| * bun scripts/import-components.ts --file <path> | ||
| * |
There was a problem hiding this comment.
The header comment says the script imports “CSV/JSON files”, but the implementation reads line-delimited JSON (JSON.parse per line) and doesn’t handle CSV. Update the comment to JSONL, or implement actual CSV parsing.
| test("base parts return is_base=true and is_extended=false", () => { | ||
| const result = getComponentTypeFlags("base") | ||
| expect(result.is_base).toBe(true) | ||
| expect(result.is_extended).toBe(false) | ||
| expect(result.is_extended_promotional).toBe(false) | ||
| }) |
There was a problem hiding this comment.
This test file references properties (is_base, is_extended) that don't match the actual function interface. The getComponentTypeFlags function returns is_basic, is_preferred, and is_extended_promotional, but this test expects is_base and is_extended properties, indicating inconsistent API design and variable naming.
| test("base parts return is_base=true and is_extended=false", () => { | |
| const result = getComponentTypeFlags("base") | |
| expect(result.is_base).toBe(true) | |
| expect(result.is_extended).toBe(false) | |
| expect(result.is_extended_promotional).toBe(false) | |
| }) | |
| test("base parts return is_basic=true and is_preferred=false", () => { | |
| const result = getComponentTypeFlags("base") | |
| expect(result.is_basic).toBe(true) | |
| expect(result.is_preferred).toBe(false) | |
| expect(result.is_extended_promotional).toBe(false) | |
| }) | |
Spotted by Graphite (based on custom rule: Custom rule)
Is this helpful? React 👍 or 👎 to let us know.
Automated fix for #92 — implemented by CashClaw agent.
Closes #92