Skip to content

feat: migrate from JSON/bun to pnpm/SQLite/vitest - #1

Merged
nazozokc merged 29 commits into
mainfrom
feat/sqlite
Jun 13, 2026
Merged

feat: migrate from JSON/bun to pnpm/SQLite/vitest#1
nazozokc merged 29 commits into
mainfrom
feat/sqlite

Conversation

@nazozokc

@nazozokc nazozokc commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Summary

Complete rewrite of the storage layer and toolchain migration.

Changes

Storage: JSON file → SQLite (better-sqlite3) with proper schema (subscriptions, tags, subscription_tags with ON DELETE CASCADE)

Toolchain:

  • bun → pnpm (package manager)
  • bun:sqlite → better-sqlite3
  • bun:test → vitest
  • bun CLI → tsx (for dev)

Bug fixes:

  • Table display: missing tags column (head 4列, rows 3列)
  • JPY currency symbol: backslash → ¥
  • Delete cascade: subscription_tags now cleaned up on delete
  • Error handling added to all DB operations

New files: 28 tests (basefs CRUD + table display), CI workflow (Node 20/22/23), Renovate config

Refactoring: lazy DB singleton, env var override (SUBSC_CLI_DB_DIR), in-memory DB injection for tests

Verification

  • pnpm build — tsdown builds successfully
  • pnpm test — 28 tests pass
  • pnpm start --help — CLI starts correctly

Summary by CodeRabbit

  • New Features

    • Database now uses SQLite for persistent data storage instead of JSON files.
    • Enhanced tag filtering with AND logic for more precise subscription searches.
    • Added validation for subscription price inputs to reject invalid amounts.
    • Improved CLI selector display for currencies and billing cycles.
  • Chores

    • Migrated project runtime from Bun to Node.js with pnpm package manager.
    • Added comprehensive automated test suite for data operations and table display.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@nazozokc, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 48 minutes and 15 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 69cbe047-5343-49a2-8732-12113167338c

📥 Commits

Reviewing files that changed from the base of the PR and between f80c8db and e1b59f9.

📒 Files selected for processing (5)
  • .github/workflows/ci.yml
  • subsc-cli/src/basefs.test.ts
  • subsc-cli/src/basefs.ts
  • subsc-cli/src/index.ts
  • subsc-cli/src/table.test.ts
📝 Walkthrough

Walkthrough

This PR migrates the subsc-cli project from Bun to Node.js + pnpm, replacing JSON-based subscription storage with SQLite via better-sqlite3. The changes include a new GitHub Actions CI pipeline, updated package configuration, a complete storage layer rewrite, and CLI/display updates to integrate the new API.

Changes

Bun to Node.js + SQLite Migration

Layer / File(s) Summary
Toolchain & Configuration Setup
.github/workflows/ci.yml, renovate.json, subsc-cli/.gitignore, subsc-cli/.npmignore, subsc-cli/package.json, subsc-cli/pnpm-workspace.yaml, subsc-cli/CLAUDE.md, subsc-cli/tsconfig.json
GitHub Actions CI workflow for Node 20/22/23 matrix with pnpm caching and smoke tests; Renovate config for automated dependency updates; package.json declares pnpm v11.5.1, adds better-sqlite3/tsx/vitest, and removes Bun dependencies; pnpm-workspace.yaml enables native builds for better-sqlite3; development guidance switches from Bun to Node.js conventions; tsconfig.json uses Node typings instead of Bun.
SQLite Storage Implementation
subsc-cli/src/basefs.ts
Replaces JSON file storage with SQLite-backed persistence using better-sqlite3 singleton with WAL mode; initializes subscriptions/tags/subscription_tags tables with cascading deletes; exports AddSharedArgs type for new write API; implements getSubscriptions() with tag hydration, writeSubscription() for atomic insert+tag linking, deleteSubscription() with warnings, and tagsSubscription() supporting single/array tags with AND logic; includes test utilities __setDb() and __resetDb().
Storage Test Suite
subsc-cli/src/basefs.test.ts
Vitest suite with isolated in-memory SQLite database; validates empty/full retrieval, write operations with tag persistence (including empty tags), supported currencies (USD/JPY) and cycles (monthly/yearly), cascading deletion, tag-based filtering with AND semantics for multiple tags, data type contracts (numeric id/price, array tags, enum currency/cycle), and tag isolation across subscriptions.
CLI Command Integration
subsc-cli/src/index.ts
Updates add command to use Inquirer input/select APIs with numeric validation for payment amount, selector name fields for currency/cycle choices; writes single subscription directly via writeSubscription({...}) instead of array append; updates tags command to accept <taglist...> and call tagsSubscription(taglist) for filtering.
Display Formatting & Tests
subsc-cli/src/table.ts, subsc-cli/src/table.test.ts
Tag rendering joins with ", " instead of `"

Sequence Diagram

sequenceDiagram
  participant CLI as CLI add command
  participant Inquirer as Inquirer prompts
  participant WriteAPI as writeSubscription()
  participant DB as SQLite database
  participant Display as spreadSubscription()
  CLI->>Inquirer: collect name/price/currency/cycle/tags
  Inquirer->>WriteAPI: provide AddSharedArgs
  WriteAPI->>DB: INSERT subscription
  WriteAPI->>DB: INSERT OR IGNORE tags
  WriteAPI->>DB: INSERT subscription_tags relations
  WriteAPI->>Display: subscription written
  Display->>Display: format table with ¥/$ symbols
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

A rabbit hops from Bun to Node so bright,
SQLite tables shining in the light,
Tags dance with pnpm, workflows run true,
Tests verify each path, old storage bid adieu.
From JSON files to database delight! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: migrating the project from JSON file storage and Bun tooling to SQLite database, pnpm package manager, and vitest testing framework.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sqlite

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

nazozokc added 2 commits June 14, 2026 00:00
pnpm v11 requires Node.js v22.13+, so Node.js 20 is no longer
supported. Removing it from the CI matrix to fix CI failures.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 6

🧹 Nitpick comments (2)
subsc-cli/src/table.test.ts (1)

25-35: ⚡ Quick win

Type makeSub as SharedArgs to keep fixtures contract-safe under strict TS.

Using Record<string, unknown> weakens compile-time checks for overridden fields in tests.

Suggested fix
+import type { SharedArgs } from "./basefs"
 
-function makeSub(overrides: Record<string, unknown> = {}) {
+function makeSub(overrides: Partial<SharedArgs> = {}): SharedArgs {
   return {
     id: 1,
     name: "Test Service",
     price: 1000,
     currency: "JPY",
     cycle: "monthly",
     tags: [],
     ...overrides,
-  };
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subsc-cli/src/table.test.ts` around lines 25 - 35, The fixture factory
makeSub should use the SharedArgs type instead of Record<string, unknown>:
change its signature to makeSub(overrides: Partial<SharedArgs> = {}): SharedArgs
(and import SharedArgs if not already imported), update the returned object type
to satisfy SharedArgs (keep the existing default fields and spread overrides) so
TypeScript validates override keys and types under strict mode.
subsc-cli/src/index.ts (1)

3-3: ⚡ Quick win

Shared style drift: import semicolons violate the repo’s ESM import/export rule. Apply the same no-semicolon import style consistently in both changed sites.

  • subsc-cli/src/index.ts#L3-L3: remove the trailing semicolon from the changed import.
  • subsc-cli/src/table.test.ts#L1-L3: remove trailing semicolons from all changed imports.

As per coding guidelines: "subsc-cli/**/*.{ts,tsx,js,jsx} ... Use no semicolons in imports/exports (ESM style)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subsc-cli/src/index.ts` at line 3, Remove the trailing semicolons from the
ESM import lines to match the repo style: in subsc-cli/src/index.ts (lines 3-3)
change the import "import { input, select } from \"`@inquirer/prompts`\";" to omit
the trailing semicolon, and in subsc-cli/src/table.test.ts (lines 1-3) remove
trailing semicolons from the changed import statements on those lines so all
imports use the no-semicolon ESM style.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 6-11: The workflow's path filter currently limits runs to
"subsc-cli/**" (the push and pull_request "paths" keys), so edits to the
workflow itself won't trigger CI; update the push and pull_request "paths"
configuration (or remove path filtering entirely) to include the workflow files
(add ".github/workflows/**") or delete the restrictive "paths" entries so
changes to the CI config will run the workflow; ensure changes are applied to
both the push "paths" block and the pull_request "paths" block.
- Around line 26-36: Pin the three mutable action references and disable
credential persistence: replace actions/checkout@v4, pnpm/action-setup@v4 and
actions/setup-node@v4 with their respective full commit SHAs (use the commit SHA
for the desired release tags) and add persist-credentials: false to the
actions/checkout step to prevent credential leakage; additionally, update the
workflow trigger paths (the push/pull_request paths filter) to include
.github/workflows/** (or remove the restrictive paths filter) so changes to
workflow files will trigger CI.

In `@subsc-cli/src/basefs.ts`:
- Around line 173-189: Deduplicate the incoming tags before building the SQL so
duplicate filter args don't incorrectly increase the required COUNT; replace
uses of the local tags array with a deduped array (e.g., uniqueTags =
Array.from(new Set(tags))) and check uniqueTags.length === 0, build placeholders
from uniqueTags, and call .all(...uniqueTags, uniqueTags.length) and use
uniqueTags.length in the HAVING COUNT(...) parameter so the COUNT(DISTINCT ...)
comparison matches the number of distinct requested tags.
- Around line 30-58: Enable SQLite foreign key enforcement and add the missing
test assertion: in subsc-cli/src/basefs.ts (lines 30-58) after creating _db and
setting pragma journal_mode, execute PRAGMA foreign_keys = ON on the Database
instance (ensure _db.pragma or _db.exec is used to enable foreign keys so the
subscription_tags FOREIGN KEY ... ON DELETE CASCADE takes effect); in
subsc-cli/src/basefs.test.ts (lines 5-6) where the in-memory testDb is created,
also enable PRAGMA foreign_keys = ON on that test DB instance; and in
subsc-cli/src/basefs.test.ts (lines 161-176) extend the deletion test to assert
that subscription_tags rows for the deleted subscription are removed (e.g.,
query subscription_tags for the deleted subscription_id and assert the result
set is empty) while leaving the existing getSubscriptions() assertion intact.
- Around line 113-147: The writeSubscription() flow currently inserts via
insertSub then inserts tags and subscription_tags (using insertTag, getTagId,
insertRel) without a transaction and can leave a partially persisted
subscription; wrap the entire sequence (insertSub + tag deduping + inserting
relations) in a single DB transaction so either everything commits or nothing
does, and before inserting relation rows dedupe the data.tags array (e.g.,
unique set) to avoid redundant insertTag/getTagId/insertRel calls and potential
uniqueness constraint failures on subscription_tags; ensure you still use INSERT
OR IGNORE for tags and then query tag ids with getTagId inside the same
transaction so relations use the correct ids.

In `@subsc-cli/src/index.ts`:
- Around line 26-30: The validate function for the amount input currently treats
empty string as 0 because Number("") === 0; update the validator in
subsc-cli/src/index.ts (the validate callback) to first reject blank input
(e.g., if value.trim() === '' return an error) before converting to Number, and
change the error message from "Please enter a valid positive number" to reflect
that zero is allowed (for example "Please enter a valid non-negative number");
keep the existing numeric check (isNaN/Number(value) < 0) after the blank check.

---

Nitpick comments:
In `@subsc-cli/src/index.ts`:
- Line 3: Remove the trailing semicolons from the ESM import lines to match the
repo style: in subsc-cli/src/index.ts (lines 3-3) change the import "import {
input, select } from \"`@inquirer/prompts`\";" to omit the trailing semicolon, and
in subsc-cli/src/table.test.ts (lines 1-3) remove trailing semicolons from the
changed import statements on those lines so all imports use the no-semicolon ESM
style.

In `@subsc-cli/src/table.test.ts`:
- Around line 25-35: The fixture factory makeSub should use the SharedArgs type
instead of Record<string, unknown>: change its signature to makeSub(overrides:
Partial<SharedArgs> = {}): SharedArgs (and import SharedArgs if not already
imported), update the returned object type to satisfy SharedArgs (keep the
existing default fields and spread overrides) so TypeScript validates override
keys and types under strict mode.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f92bb702-df19-49ea-9451-1515eb117493

📥 Commits

Reviewing files that changed from the base of the PR and between 5cfe0f8 and f80c8db.

⛔ Files ignored due to path filters (2)
  • subsc-cli/bun.lock is excluded by !**/*.lock
  • subsc-cli/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • .github/workflows/ci.yml
  • renovate.json
  • subsc-cli/.gitignore
  • subsc-cli/.npmignore
  • subsc-cli/CLAUDE.md
  • subsc-cli/package.json
  • subsc-cli/pnpm-workspace.yaml
  • subsc-cli/src/basefs.test.ts
  • subsc-cli/src/basefs.ts
  • subsc-cli/src/calc.ts
  • subsc-cli/src/index.ts
  • subsc-cli/src/table.test.ts
  • subsc-cli/src/table.ts
  • subsc-cli/tsconfig.json

Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/ci.yml
Comment on lines +26 to +36
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 11

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
cache-dependency-path: "subsc-cli/pnpm-lock.yaml"

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
echo "=== File: $FILE ==="
if [ ! -f "$FILE" ]; then
  echo "Missing $FILE" >&2
  exit 1
fi

wc -l "$FILE"
nl -ba "$FILE" | sed -n '1,160p'

echo
echo "=== Search for checkout persist-credentials ==="
rg -n "persist-credentials" "$FILE" || true

echo
echo "=== Search for pinning to full commit SHAs for actions ==="
rg -n "uses:\s*(actions/|pnpm/|.*`@v`)" "$FILE" || true

echo
echo "=== Show trigger block ==="
# Extract on: block roughly (from first 'on:' to next top-level key)
awk '
  /^on:/ {in_on=1}
  in_on {print}
  in_on && /^[a-zA-Z0-9_ -]+:/ && $1!="on:" {exit}
' "$FILE" | sed -n '1,120p' || true

Repository: nazozokc/subsc-cli

Length of output: 191


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
echo "=== File: $FILE ==="
[ -f "$FILE" ] || { echo "Missing $FILE" >&2; exit 1; }

echo "--- Line count ---"
wc -l "$FILE"

echo
echo "--- File preview (1-160) with line numbers via awk ---"
awk 'NR<=160{printf "%4d: %s\n", NR, $0}' "$FILE"

echo
echo "=== Search for checkout persist-credentials ==="
rg -n "persist-credentials" "$FILE" || true

echo
echo "=== Search for unpinned action refs (major-only like `@v4`) ==="
rg -n "uses:\s*[^#\n]+@v[0-9]+(\b|$)" "$FILE" || true

echo
echo "=== Search for action refs pinned to full SHAs (40 hex) ==="
rg -n "uses:\s*[^#\n]+@[0-9a-f]{40}\b" "$FILE" || true

echo
echo "--- Show trigger block around `on:` ---"
awk '
  /^on:/ {print; in_on=1; next}
  in_on {
    # stop when a top-level key starts (no indentation) that's not part of on:
    if ($0 ~ /^[a-zA-Z0-9_ -]+:/ && $0 !~ /^on:/ && $0 !~ /^  /) {exit}
    print
  }
' "$FILE" | sed -n '1,160p'

Repository: nazozokc/subsc-cli

Length of output: 1913


🌐 Web query:

GitHub Actions actions/checkout persist-credentials default value

💡 Result:

The default value for the persist-credentials input in the GitHub Actions actions/checkout action is true [1][2]. When set to true (the default), the action configures the authentication token or SSH key used for the checkout in the local git configuration [1][2]. This allows your workflow scripts to perform authenticated git operations (such as git fetch or git push) without requiring additional manual configuration [3][4]. The action automatically removes these credentials from the local git configuration during the post-job cleanup phase [3][4]. If you wish to disable this behavior, you can explicitly set persist-credentials to false in your workflow file [3][5].

Citations:


🌐 Web query:

GitHub Actions workflow pathsfilter trigger when only.github/workflows/** changes

💡 Result:

Yes, you can configure a GitHub Actions workflow to trigger when changes are made to files within the.github/workflows/ directory by using the paths filter in your workflow configuration [1][2]. To trigger a workflow only when files in the.github/workflows/ directory change, use the following syntax in your workflow YAML file [1]: on: push: paths: - '.github/workflows/' This configuration ensures that the workflow will run if any file within the.github/workflows/ directory is modified in a push event [1]. You can use glob patterns, such as the double asterisk, to match all files recursively within that directory [3][4]. Key considerations: 1. Trigger Event: The paths filter works with the push and pull_request events [5][3]. 2. Path Filters and Other Triggers: If you have other triggers defined in the same workflow (such as schedule or workflow_dispatch), the paths filter only applies to the push or pull_request events it is associated with [1]. 3. Exclusions: If you need to trigger on changes to the workflow directory but ignore specific files within it, you cannot combine paths and paths-ignore for the same event [3]. Instead, you can use the! character within the paths filter to exclude specific files [5][3]. If you need more granular control—such as triggering based on specific workflow files while excluding others, or applying logic at the job or step level—you may consider using a third-party action like dorny/paths-filter, which allows for conditional execution based on file changes [6][7].

Citations:


Pin GitHub Actions to commit SHAs and disable checkout credential persistence.

  • In .github/workflows/ci.yml, actions/checkout@v4 / pnpm/action-setup@v4 / actions/setup-node@v4 are only tagged (mutable); pin each to a full commit SHA.
  • Add persist-credentials: false to the actions/checkout step (current default is true).
  • CI won’t run for changes limited to .github/workflows/** because on: push/pull_request paths is restricted to subsc-cli/**.
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 26-26: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 26-26: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 28-28: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 32-32: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 26 - 36, Pin the three mutable action
references and disable credential persistence: replace actions/checkout@v4,
pnpm/action-setup@v4 and actions/setup-node@v4 with their respective full commit
SHAs (use the commit SHA for the desired release tags) and add
persist-credentials: false to the actions/checkout step to prevent credential
leakage; additionally, update the workflow trigger paths (the push/pull_request
paths filter) to include .github/workflows/** (or remove the restrictive paths
filter) so changes to workflow files will trigger CI.

Source: Linters/SAST tools

Comment thread subsc-cli/src/basefs.ts
Comment thread subsc-cli/src/basefs.ts Outdated
Comment thread subsc-cli/src/basefs.ts Outdated
Comment thread subsc-cli/src/index.ts
- Enable PRAGMA foreign_keys = ON in production and test DB
- Wrap writeSubscription() in a transaction for atomicity
- Deduplicate tags in tagsSubscription() to prevent false negatives
- Add .github/workflows/** to CI path filter
- Remove semicolons from ESM imports per repo style
- Fix amount validate to reject blank input and correct error message
- Use Partial<SharedArgs> in makeSub fixture for type safety
- Verify subscription_tags cascade in deletion test
@nazozokc
nazozokc merged commit d96e88d into main Jun 13, 2026
7 checks passed
@nazozokc
nazozokc deleted the feat/sqlite branch June 13, 2026 15:15
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.

1 participant