Skip to content

fix: Add is_extended_promotional column to components (from da#158

Open
SyedHannanMehdi wants to merge 30 commits intotscircuit:mainfrom
SyedHannanMehdi:cashclaw/fix-92-add-is-extended-promotio
Open

fix: Add is_extended_promotional column to components (from da#158
SyedHannanMehdi wants to merge 30 commits intotscircuit:mainfrom
SyedHannanMehdi:cashclaw/fix-92-add-is-extended-promotio

Conversation

@SyedHannanMehdi
Copy link
Copy Markdown

Automated fix for #92 — implemented by CashClaw agent.

Closes #92

Copilot AI review requested due to automatic review settings March 29, 2026 20:54
Comment on lines +1 to +95
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,
})
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment thread routes/components/get.ts
Comment on lines +25 to +26
basic: component.basic === 1,
preferred: component.preferred === 1,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Suggested change
basic: component.basic === 1,
preferred: component.preferred === 1,
basic: component.is_basic === 1,
preferred: component.is_preferred === 1,

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment thread scripts/populate-components.ts Outdated
Comment on lines +118 to +119
basic,
preferred,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Suggested change
basic,
preferred,
is_basic: basic,
is_preferred: preferred,

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment thread scripts/populate-components.ts Outdated

return {
lcsc,
mfr: String(p.componentCode ?? p.lcscPartNumber ?? ""),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ?? ""),
Suggested change
mfr: String(p.componentCode ?? p.lcscPartNumber ?? ""),
mfr: String(p.brandNameEn ?? ""),

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_promotional column 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"
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
t === "extended_promotional"
t === "extended_promotional" ||
t === "expand_special"

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +50
.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) =>
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
.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) =>

Copilot uses AI. Check for mistakes.
Comment thread scripts/populate-components.ts Outdated
Comment on lines +13 to +16
import { createDb } from "../lib/db"
import { readFileSync } from "node:fs"
import { join } from "node:path"

Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +99
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,
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread routes/components/list.ts
@@ -0,0 +1,113 @@
import { z } from "zod"
import { publicProcedure } from "../../lib/trpc"
import { getDb } from "../../lib/db/get-db"
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread scripts/populate-components.ts Outdated
Comment on lines +81 to +82
} catch {
// Column already exists — that's fine
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
} 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
}

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +95
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,
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread routes/components/list.ts Outdated
Comment on lines +2 to +4
import { publicProcedure } from "../../lib/trpc"
import { getDb } from "../../lib/db/get-db"

Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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 })
},
}),
}

Copilot uses AI. Check for mistakes.
import { parseArgs } from "node:util"
import { createReadStream } from "node:fs"
import * as readline from "node:readline"
import { getDb } from "../lib/db/get-db"
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
import { getDb } from "../lib/db/get-db"
import { getDb } from "../lib/db/get-db-client"

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +6
/**
* Script to import components from JLCPCB data source CSV/JSON files.
*
* Usage:
* bun scripts/import-components.ts --file <path>
*
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +9
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)
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add is_extended_promotional column to components (from data source)

2 participants