0.0.13 - React Query, Drizzle, db logic in renderer#645
Conversation
WalkthroughReplaces large parts of the legacy DB layer and tests with a Drizzle + React Query-driven data flow, adds an SQL.js driver and unsafe SQL IPC proxy, migrates many UI components to query/mutation hooks, adds migrations/snapshots, and removes numerous legacy table modules, exports, and tests. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Renderer
participant Preload as Preload (ipcRenderer)
participant Main as Main (ipcMain)
participant DB as Drizzle DB
rect rgb(230,242,255)
note right of Renderer: Unsafe SQL proxy (new)
Renderer->>Preload: APP_API.unsafeSqlProxy(sql)
Preload->>Main: ipc.invoke("unsafeSql:proxy", sql)
Main->>DB: handleUnsafeSqlProxyWithDb(sql)
DB-->>Main: rows / error
Main-->>Preload: { rows } / error
Preload-->>Renderer: { rows } / error
end
sequenceDiagram
autonumber
actor User
participant UI as MusicXmlSelector
participant Hook as useImportMusicXml (mutation)
participant DB as Drizzle DB
participant Cache as QueryClient
User->>UI: select MusicXML file
UI->>Hook: mutate({ file, allPages, measures, allBeats })
Hook->>DB: begin transaction: replace measures/beats, create beats, update pages
DB-->>Hook: success / error
alt success
Hook->>Cache: invalidate measures/beats/pages
Hook-->>UI: success message
else error
Hook-->>UI: error message
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 30
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (18)
apps/desktop/src/components/inspector/PageEditor.tsx (1)
61-66: Fix first-page detection: compare IDs, not object refs.Object identity is brittle; different instances representing the same page will fail this check, causing incorrect disabling and display.
- if (pages.length > 0) { - const firstPage = pages[0]; - setIsFirstPage(selectedPage === firstPage); - } + if (pages.length > 0) { + const firstPage = pages[0]; + setIsFirstPage(!!selectedPage && selectedPage.id === firstPage.id); + }apps/desktop/i18n/es.json (2)
196-199: Placeholder name mismatch: use {status}, not {estado}.This will throw at runtime or during i18n compile/validation.
Apply this diff:
- "marcher": "Marcher: {estado}", + "marcher": "Marcher: {status}",
1-756: Add missing export keys and fix ICU/placeholder issues in es.json
- Missing keys — add exportCoordinates.marchersNotLoaded and exportCoordinates.marcherPagesNotLoaded to apps/desktop/i18n/es.json (these are present in en.json).
- Invalid ICU plural — apps/desktop/i18n/es.json:27 (setSelectedToPreviousSuccess) contains a malformed double-brace plural ("{{count, plural...}"); fix to a valid ICU plural pattern ({count, plural, one {…} other {…}}). Also check apps/desktop/i18n/pt-BR.json:27.
- Placeholder name mismatch — apps/desktop/i18n/es.json:197 ("marcher": "Marcher: {estado}") uses {estado}; rename to the placeholder used in en.json (e.g., {status}) so runtime interpolation matches.
apps/desktop/i18n/en.json (1)
1-762: Fix ICU plural syntax and placeholder name mismatch in i18n locales
- Invalid ICU plural syntax (extra "{") in apps/desktop/i18n/es.json and apps/desktop/i18n/pt-BR.json at actions.batchEdit.setSelectedToPreviousSuccess — contains "{{count, plural,...}}". Replace the double "{{" with a single "{" so it's "{count, plural,...}".
- Placeholder mismatch: apps/desktop/i18n/es.json exportCoordinates.marcher uses {estado} while en.json (and other locales) use {status}. Rename {estado} → {status} so placeholder names are consistent.
apps/desktop/i18n/pt-BR.json (1)
1-756: Add missing exportCoordinates keys and fix ICU plural formatting
- Add exportCoordinates.marchersNotLoaded and exportCoordinates.marcherPagesNotLoaded to apps/desktop/i18n/pt-BR.json to match en.json.
- Fix the invalid double-brace ICU plural occurrences at:
- apps/desktop/i18n/pt-BR.json — actions.batchEdit.setSelectedToPreviousSuccess (line ~27)
- apps/desktop/i18n/es.json — actions.batchEdit.setSelectedToPreviousSuccess (line ~27)
Replace the malformed "{{count, plural…}}" insertion with a proper ICU plural block.apps/desktop/src/components/marcher/NewMarcherForm.tsx (3)
80-112: Guard against NaN drillOrder; prevents invalid insertIf the user clears the drill order field, parseInt('') yields NaN. That flows into Set/loop math and can reach the DB as NaN (violates integer schema).
Apply this diff to short‑circuit invalid input before building payloads:
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { - setContent(<MarcherListContents />, "marchers"); event.preventDefault(); + // Navigate after we know we submitted (optional) + setContent(<MarcherListContents />, "marchers"); + if (!Number.isFinite(drillOrder)) { + setDrillOrderError(t("marchers.drillOrderError.invalid")); + return; + } let newDrillOrderOffset = 0; const existingDrillOrders = new Set<number>( (marchers || []) .filter( (marcher: Marcher) => marcher.drill_prefix === drillPrefix, ) .map((marcher: Marcher) => marcher.drill_order), );Additionally, update validation to treat non‑numeric values as invalid (see separate comment below).
138-154: Validation gap: treat non-numeric drill orders as errorsWithout this, NaN passes validation and enables submit.
const validateDrillOrder = useCallback( (drillOrder: number) => { + if (!Number.isFinite(drillOrder)) { + setDrillOrderError(t("marchers.drillOrderError.required")); + return; + } const existingMarchers = (marchers || []).filter( (marcher: Marcher) => marcher.drill_prefix === drillPrefix, );Also consider disabling submit if drillOrder is not finite (see setSubmitIsDisabled effect).
85-90: Don’t store translated section labels in the DBOn first load you set section to a translated label (defaultSection(t)), but you persist section as-is in the payload. If the user never changes the section, you’ll store localized strings (e.g., “Otro”) instead of canonical keys (“Other”).
Recommended approach: keep state as canonical section keys; translate only for display. Example changes (outside the exact ranges shown):
// near line 33 const DEFAULT_SECTION_KEY = "Other"; // lines 64 and 68 setSection(DEFAULT_SECTION_KEY); // line 245 (label) <SelectTriggerButton label={getTranslatedSectionName(section || DEFAULT_SECTION_KEY, t)} /> // line 206 (submit disabled) section === DEFAULT_SECTION_KEY || section === undefined // payload (lines 103-107) newMarchers.push({ section: section || DEFAULT_SECTION_KEY, drill_prefix: drillPrefix, drill_order: newDrillOrder, });Also applies to: 103-107, 63-69
apps/desktop/electron/main/services/export-utility-process.ts (1)
5-5: Fix build blockers: remove unused import/variable.
pathis imported but never used;organizeBySectionis destructured but unused. This can fail builds with noUnusedLocals.-import path from "path"; ... - const { dbPath, filePath, quarterPages, organizeBySection, ...options } = - payload; + const { dbPath, filePath, quarterPages, ...options } = payload;Also applies to: 156-158
apps/desktop/src/components/marcher/__test__/NewMarcherForm.test.tsx (1)
55-65: Fix tests that mock a removed Marcher store
- Tests and mocks still import/vi.mock "@/stores/MarcherStore" and "@/stores/marcher/useMarcherStore" but there is no Marcher store implementation under apps/desktop/src/stores — this will break test compilation. Affected files: apps/desktop/src/components/marcher/test/NewMarcherForm.test.tsx, apps/desktop/src/components/marcher/test/MarcherList.test.tsx, apps/desktop/src/mocks/components/ChangeMarchersSelection.mock.tsx.
- Replace these mocks with the new React Query hooks (mock the query/mutation hooks) and wrap affected tests with a QueryClientProvider — or restore/fix the module path before re‑enabling the suites.
apps/desktop/electron/database/tables/__test__/testUtils.ts (1)
1-61: Move test helper out of legacy electron/database/tables/testThis path is legacy — tests/utilities under apps/desktop/electron/database/tables are being removed. Move apps/desktop/electron/database/tables/test/testUtils.ts to apps/desktop/src/global/classes/test/ or a shared test-utils module and update all imports accordingly. Can draft a follow-up PR to move the file and update imports.
apps/desktop/electron/main/index.ts (2)
585-615: Guard dialog cancellation before accessing filePaths[0]Accessing path.filePaths[0] when the dialog is canceled throws. The check is currently commented out.
.then(async (path) => { - // If the user cancels the dialog, and there is no previous path, return -1 - // if (path.canceled || !path.filePaths[0]) return -1; + if (path.canceled || !path.filePaths[0]) return -1; DatabaseServices.setDbPath(path.filePaths[0]);
709-737: Fix order: check cancellation before reading filereadFileSync(path.filePaths[0]) runs even when the dialog is canceled.
.then(async (path) => { - const fileContents = fs.readFileSync(path.filePaths[0]); - const jsonStr = fileContents.toString(); - await updateFieldsPropertiesJSON(jsonStr); - - // If the user cancels the dialog, and there is no previous path, return -1 - if (path.canceled || !path.filePaths[0]) return -1; + if (path.canceled || !path.filePaths[0]) return -1; + const fileContents = fs.readFileSync(path.filePaths[0]); + const jsonStr = fileContents.toString(); + await updateFieldsPropertiesJSON(jsonStr); win?.webContents.send("field_properties:onImport");apps/desktop/electron/database/database.services.ts (2)
146-184: Implement the "values" method for the Drizzle proxyDrizzle can call the "values" method. Not handling it will throw at runtime.
switch (method) { case "all": rows = result.all(...params); return { rows: rows ? rows.map((row: { [key: string]: any }) => Object.values(row)) : [], }; case "get": rows = result.get(...params); return { rows: rows ? Object.values(rows as Record<string, any>) : undefined, }; + case "values": + rows = result.all(...params); + return { + rows: rows + ? rows.map((row: { [key: string]: any }) => Object.values(row)) + : [], + }; case "run": rows = result.run(...params); return { rows: [] }; default: throw new Error(`Unknown method: ${method}`); }
245-264: Gate the unsafe SQL IPC in non‑production buildsExposing arbitrary exec() to the renderer increases risk if the renderer is compromised. Restrict to test/dev via an env flag.
export function initHandlers() { // Generic SQL proxy handler for Drizzle ORM ipcMain.handle("sql:proxy", handleSqlProxy); - ipcMain.handle("unsafeSql:proxy", handleUnsafeSqlProxy); + if (process.env.ENABLE_UNSAFE_SQL_PROXY === "1") { + ipcMain.handle("unsafeSql:proxy", handleUnsafeSqlProxy); + }Also ensure preload only calls unsafeSqlProxy when the flag is enabled.
apps/desktop/electron/preload/index.ts (1)
220-243: History IPC handlers missing — implement handlers or remove exposed APIsPreload exposes history:* methods (apps/desktop/electron/preload/index.ts lines 220-243) but no ipcMain handlers for history:* were found; these ipcRenderer invocations will reject at runtime. Add matching ipcMain.handle/on implementations in the main/database service or remove these methods from preload.
apps/desktop/electron/database/migrations/schema.ts (2)
265-271: Use browser-safe blob type for audio_files.datablob() maps to Buffer in Drizzle; Buffer isn’t available in the browser/SQL.js. You already defined browserSafeBinaryBlob; use it here to avoid web build issues.
Apply:
export const audio_files = sqliteTable("audio_files", { id: integer().primaryKey(), path: text().notNull(), nickname: text(), - data: blob(), + data: browserSafeBinaryBlob(), selected: integer().default(0).notNull(),Note: If this is the last use of blob(), remove blob from the sqlite-core import to avoid unused import warnings.
316-333: Schema/migration drift — make position_order NOT NULL in the migrationapps/desktop/electron/database/migrations/schema.ts (lines 316–333) declares shape_page_marchers.position_order as .notNull(), but apps/desktop/electron/database/migrations/0007_misty_zemo.sql (CREATE TABLE __new_shape_page_marchers around lines ~135–153) defines position_order as nullable; this allows multiple NULLs despite the UNIQUE (shape_page_id, position_order) index. Update 0007 (and any other SQL that recreates this table) to declare position_order INTEGER NOT NULL and backfill existing NULLs (e.g., assign sequential position_order per shape_page_id) before enforcing NOT NULL — or, if NULL is intended, remove .notNull() from schema.ts.
| const updatedItem = await tx | ||
| .update(schema.table_name) | ||
| .set(updateData) | ||
| .where(eq(schema.table_name.id, id)) | ||
| .returning() | ||
| .get(); | ||
| updatedItems.push( |
There was a problem hiding this comment.
Drizzle SQLite: .returning().get() is invalid; destructure the returned rows instead.
After .returning(), Drizzle returns an array. Use array destructuring to get the single updated row.
- const updatedItem = await tx
- .update(schema.table_name)
- .set(updateData)
- .where(eq(schema.table_name.id, id))
- .returning()
- .get();
+ const [updatedItem] = await tx
+ .update(schema.table_name)
+ .set(updateData)
+ .where(eq(schema.table_name.id, id))
+ .returning();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const updatedItem = await tx | |
| .update(schema.table_name) | |
| .set(updateData) | |
| .where(eq(schema.table_name.id, id)) | |
| .returning() | |
| .get(); | |
| updatedItems.push( | |
| const [updatedItem] = await tx | |
| .update(schema.table_name) | |
| .set(updateData) | |
| .where(eq(schema.table_name.id, id)) | |
| .returning(); | |
| updatedItems.push( |
🤖 Prompt for AI Agents
In .cursor/rules/basic-database-interaction.mdc around lines 197-203, the call
chain uses `.returning().get()` which is invalid for Drizzle SQLite because
`.returning()` yields an array of rows; change the code to await the update with
`.returning()` (no `.get()`), destructure the resulting array (e.g., `const
[updatedItem] = await ...returning()`), and then push that `updatedItem` into
`updatedItems`.
| qc.invalidateQueries({ | ||
| queryKey: Array.from(itemIds).map((id) => | ||
| {tableName}Keys.byId(id), | ||
| ), | ||
| }); | ||
| qc.invalidateQueries({ | ||
| queryKey: [KEY_BASE], | ||
| }); |
There was a problem hiding this comment.
React Query v5 invalidateQueries: don’t pass an array of keys to queryKey.
queryKey accepts a single key (array), not an array of keys. Invalidate per id or use a predicate.
- qc.invalidateQueries({
- queryKey: Array.from(itemIds).map((id) =>
- {tableName}Keys.byId(id),
- ),
- });
+ for (const id of itemIds) {
+ qc.invalidateQueries({ queryKey: {tableName}Keys.byId(id) });
+ }Alternative: use a predicate that matches id-specific keys if you have a stable key shape.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| qc.invalidateQueries({ | |
| queryKey: Array.from(itemIds).map((id) => | |
| {tableName}Keys.byId(id), | |
| ), | |
| }); | |
| qc.invalidateQueries({ | |
| queryKey: [KEY_BASE], | |
| }); | |
| for (const id of itemIds) { | |
| qc.invalidateQueries({ queryKey: {tableName}Keys.byId(id) }); | |
| } | |
| qc.invalidateQueries({ | |
| queryKey: [KEY_BASE], | |
| }); |
🤖 Prompt for AI Agents
In .cursor/rules/basic-database-interaction.mdc around lines 352 to 359, the
calls to qc.invalidateQueries are passing an array-of-keys (an array of arrays)
as queryKey which is invalid in React Query v5; change this to either call
invalidateQueries once per id with the single queryKey shape (e.g., loop
Array.from(itemIds) and call qc.invalidateQueries({ queryKey:
{tableName}Keys.byId(id) }) ), or replace the multi-key call with a single
invalidateQueries using a predicate that matches id-specific keys
(qc.invalidateQueries({ predicate: q => /* match ids by q.queryKey shape */ }))
and for the base key call invalidateQueries with the single base key
(qc.invalidateQueries({ queryKey: KEY_BASE })) so each invalidateQuery receives
one key/array rather than an array of keys.
| extends: [ | ||
| "eslint:recommended", | ||
| "react-app", | ||
| "plugin:astro/recommended", | ||
| "plugin:@tanstack/query/recommended", | ||
| ], |
There was a problem hiding this comment.
🧩 Analysis chain
Good add: TanStack Query linting; verify plugin is installed across workspaces
Extends entry is correct. Please ensure @tanstack/eslint-plugin-query is present in all relevant package.json files.
Run:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Packages missing @tanstack/eslint-plugin-query:"
fd -H -a package.json | while read -r f; do
has=$(jq -r '(.devDependencies["@tanstack/eslint-plugin-query"] // .dependencies["@tanstack/eslint-plugin-query"]) // empty' "$f")
if [[ -z "$has" ]]; then
echo " - $f"
fi
done
echo
echo "Places extending plugin:@tanstack/query/recommended:"
rg -nP 'plugin:@tanstack/query/recommended' -C2Length of output: 742
Add @tanstack/eslint-plugin-query to the workspace (missing in multiple package.json)
Extends entry plugin:@tanstack/query/recommended is fine. @tanstack/eslint-plugin-query is not declared in these package.json files:
- actions/desktop-deploy/package.json
- actions/desktop-deploy/test/app-in-root/package.json
- actions/desktop-deploy/test/app-in-subdirectory/package.json
- apps/desktop/package.json
- apps/website/package.json
- packages/config/package.json
- packages/core/package.json
- packages/musicxml-parser/package.json
- packages/path-utility/example/package.json
- packages/path-utility/package.json
- packages/ui/package.json
Add @tanstack/eslint-plugin-query as a devDependency (preferably at the monorepo root or in packages that run ESLint) to avoid broken linting.
🤖 Prompt for AI Agents
.eslintrc.cjs around lines 3 to 8: the config extends
"plugin:@tanstack/query/recommended" but the corresponding plugin package
(@tanstack/eslint-plugin-query) is missing from multiple package.json files; add
"@tanstack/eslint-plugin-query" as a devDependency—preferably in the monorepo
root package.json (or in each workspace package that runs ESLint:
actions/desktop-deploy, its test app packages, apps/desktop, apps/website,
packages/config, packages/core, packages/musicxml-parser, packages/path-utility
and packages/ui)—then run your workspace package manager install (pnpm/yarn/npm)
so ESLint can resolve the plugin.
| # metronome test output | ||
| packages/metronome/ | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
Anchor path to repo root; verify we won’t ignore a real package unintentionally
Using a leading slash avoids matching nested paths and makes intent explicit. Also, confirm this directory isn’t (or won’t be) a real package with tracked sources.
Apply this diff to anchor to the repo root:
-packages/metronome/
+/packages/metronome/Run to verify no tracked files would be hidden:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Tracked files under packages/metronome:"
git ls-files 'packages/metronome' | wc -l
echo
echo "Show any tracked files (should be empty):"
git ls-files 'packages/metronome' || true
echo
echo "Ignored status for the path (sanity check):"
git check-ignore -v packages/metronome/ || trueLength of output: 714
Remove or narrow the .gitignore entry for packages/metronome/ — it contains tracked files
git ls-files shows 15 tracked files under packages/metronome (package.json, src/, tsconfig.json, etc.); don’t ignore the whole package directory. Either remove the line or replace it with a root-anchored, specific ignore for only generated/test output (e.g. /packages/metronome//). File: .gitignore (lines 48–50).
🤖 Prompt for AI Agents
In .gitignore around lines 48–50, the entry "packages/metronome/" wrongly
ignores an entire package that contains tracked source files; remove that line
or replace it with a root-anchored pattern that only ignores generated/test
output (for example "/packages/metronome/<generated-folder>/"). After updating
.gitignore, if any files in packages/metronome are already tracked and should
remain tracked, ensure you do not remove them from the index; if you intended to
stop tracking specific generated files, remove those from the repo with git rm
--cached <path> and commit the change so the narrowed ignore takes effect.
| /** Directly executes the SQL query without any parameters */ | ||
| async function handleUnsafeSqlProxy(_: any, sql: string) { | ||
| try { | ||
| return await handleUnsafeSqlProxyWithDb(connect(), sql); | ||
| } catch (error: any) { | ||
| console.error("Error from unsafe SQL proxy:", error); | ||
| throw error; | ||
| } | ||
| } |
There was a problem hiding this comment.
Close DB connection in unsafe SQL handler to avoid leaks
connect() is called but never closed, leaking a database handle.
async function handleUnsafeSqlProxy(_: any, sql: string) {
- try {
- return await handleUnsafeSqlProxyWithDb(connect(), sql);
- } catch (error: any) {
+ const db = connect();
+ try {
+ return await handleUnsafeSqlProxyWithDb(db, sql);
+ } catch (error: any) {
console.error("Error from unsafe SQL proxy:", error);
throw error;
- }
+ } finally {
+ db.close();
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** Directly executes the SQL query without any parameters */ | |
| async function handleUnsafeSqlProxy(_: any, sql: string) { | |
| try { | |
| return await handleUnsafeSqlProxyWithDb(connect(), sql); | |
| } catch (error: any) { | |
| console.error("Error from unsafe SQL proxy:", error); | |
| throw error; | |
| } | |
| } | |
| /** Directly executes the SQL query without any parameters */ | |
| async function handleUnsafeSqlProxy(_: any, sql: string) { | |
| const db = connect(); | |
| try { | |
| return await handleUnsafeSqlProxyWithDb(db, sql); | |
| } catch (error: any) { | |
| console.error("Error from unsafe SQL proxy:", error); | |
| throw error; | |
| } finally { | |
| db.close(); | |
| } | |
| } |
🤖 Prompt for AI Agents
In apps/desktop/electron/database/database.services.ts around lines 227 to 235,
the code calls connect() but never closes the returned DB handle, leaking
resources; change the function to await connect() into a local db variable, pass
that db to handleUnsafeSqlProxyWithDb, and ensure the db is closed in a finally
block (e.g., try { return await handleUnsafeSqlProxyWithDb(db, sql); } finally {
await db.close(); } or use the appropriate release method for your DB client) so
the connection is always released even on error.
| {selectedShapePageIds.map((shapePageId) => { | ||
| return ( | ||
| <div | ||
| key={marcherShape.shapePage.id} | ||
| className="flex flex-col gap-12" | ||
| > | ||
| {singleShapeEditor(marcherShape)} | ||
| <div key={shapePageId} className="flex flex-col gap-12"> | ||
| {singleShapeEditor( | ||
| marcherShapesByShapePageId[shapePageId], | ||
| )} | ||
| </div> |
There was a problem hiding this comment.
Guard against missing MarcherShape to prevent runtime crashes.
When a selected shape page has no corresponding MarcherShape on the canvas (e.g., after delete/copy or before canvas is ready), singleShapeEditor receives undefined and will crash. Guard the lookup.
Apply this diff:
- {selectedShapePageIds.map((shapePageId) => {
- return (
- <div key={shapePageId} className="flex flex-col gap-12">
- {singleShapeEditor(
- marcherShapesByShapePageId[shapePageId],
- )}
- </div>
- );
- })}
+ {selectedShapePageIds.map((shapePageId) => {
+ const ms = marcherShapesByShapePageId[shapePageId];
+ if (!ms) return null;
+ return (
+ <div key={shapePageId} className="flex flex-col gap-12">
+ {singleShapeEditor(ms)}
+ </div>
+ );
+ })}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {selectedShapePageIds.map((shapePageId) => { | |
| return ( | |
| <div | |
| key={marcherShape.shapePage.id} | |
| className="flex flex-col gap-12" | |
| > | |
| {singleShapeEditor(marcherShape)} | |
| <div key={shapePageId} className="flex flex-col gap-12"> | |
| {singleShapeEditor( | |
| marcherShapesByShapePageId[shapePageId], | |
| )} | |
| </div> | |
| {selectedShapePageIds.map((shapePageId) => { | |
| const ms = marcherShapesByShapePageId[shapePageId]; | |
| if (!ms) return null; | |
| return ( | |
| <div key={shapePageId} className="flex flex-col gap-12"> | |
| {singleShapeEditor(ms)} | |
| </div> | |
| ); | |
| })} |
🤖 Prompt for AI Agents
In apps/desktop/src/components/inspector/ShapeEditor.tsx around lines 347 to
353, the code maps selectedShapePageIds and passes
marcherShapesByShapePageId[shapePageId] directly to singleShapeEditor, which can
be undefined and crash; update the mapping to guard the lookup by checking that
marcherShapesByShapePageId[shapePageId] exists before rendering (either filter
selectedShapePageIds to those with a corresponding marcher shape or
conditionally render singleShapeEditor only when the lookup is truthy), ensuring
you keep the outer div key consistent and skip or render a safe fallback when
the shape is missing.
| const deleteMarchers = useMutation( | ||
| deleteMarchersMutationOptions(queryClient), | ||
| ).mutate; | ||
| const updateMarchers = useMutation( | ||
| updateMarchersMutationOptions(queryClient), | ||
| ).mutate; |
There was a problem hiding this comment.
Use mutateAsync + await; prevent race conditions and clear refs only on success.
useMutation().mutate is fire-and-forget. This introduces races between delete/update and clears refs even on failure. Switch to mutateAsync, await both operations, and only clear refs on success. Also no-op early when nothing changed.
- const deleteMarchers = useMutation(
- deleteMarchersMutationOptions(queryClient),
- ).mutate;
- const updateMarchers = useMutation(
- updateMarchersMutationOptions(queryClient),
- ).mutate;
+ const { mutateAsync: deleteMarchersAsync } = useMutation(
+ deleteMarchersMutationOptions(queryClient),
+ );
+ const { mutateAsync: updateMarchersAsync } = useMutation(
+ updateMarchersMutationOptions(queryClient),
+ );
@@
- async function handleSubmit() {
- setIsEditing(false);
-
- const modifiedMarchers: ModifiedMarcherArgs[] = [];
-
- for (const [pageId, changes] of Object.entries(changesRef.current))
- modifiedMarchers.push({ id: Number(pageId), ...changes });
-
- if (deletionsRef.current.length > 0) {
- const marcherIdsSet = new Set(deletionsRef.current.map((id) => id));
- await deleteMarchers(marcherIdsSet);
- }
-
- const result = updateMarchers(modifiedMarchers);
- changesRef.current = {};
- deletionsRef.current = [];
- return result;
- }
+ async function handleSubmit() {
+ setIsEditing(false);
+
+ const modifiedMarchers: ModifiedMarcherArgs[] = [];
+ for (const [pageId, changes] of Object.entries(changesRef.current)) {
+ modifiedMarchers.push({ id: Number(pageId), ...changes });
+ }
+
+ if (deletionsRef.current.length === 0 && modifiedMarchers.length === 0) {
+ return;
+ }
+
+ try {
+ if (deletionsRef.current.length > 0) {
+ const marcherIdsSet = new Set(deletionsRef.current.map((id) => id));
+ await deleteMarchersAsync(marcherIdsSet);
+ }
+ if (modifiedMarchers.length > 0) {
+ await updateMarchersAsync(modifiedMarchers);
+ }
+ // clear only after successful mutations
+ changesRef.current = {};
+ deletionsRef.current = [];
+ } catch {
+ // onError toasts fire in mutation options; keep refs for retry
+ }
+ }Also applies to: 67-84
🤖 Prompt for AI Agents
In apps/desktop/src/components/marcher/MarcherList.tsx around lines 39-44 (and
similarly for the block at 67-84), the code uses useMutation(...).mutate which
is fire-and-forget; change to useMutation(...).mutateAsync and await the calls,
add an early return/no-op when there are no marchers to update/delete, and only
clear refs (or perform UI state resets) after the awaited mutation resolves
successfully (handle errors via try/catch and do not clear refs on failure);
ensure both delete and update use await mutateAsync and run sequentially or in
Promise.all as intended so race conditions are prevented.
| ModifiedSectionAppearanceArgs, | ||
| NewSectionAppearanceArgs, | ||
| } from "@/global/classes/SectionAppearance"; | ||
| import { SectionAppearance } from "@/global/classes/SectionAppearance"; |
There was a problem hiding this comment.
Fix type mismatch: use DatabaseSectionAppearance (DB shape) instead of SectionAppearance (class).
The query returns DatabaseSectionAppearance (snake_case fields). Typing local state as SectionAppearance causes TS errors and field mismatches (likely the build failure). Import the DB type and use it for local state.
Apply this diff:
-import { SectionAppearance } from "@/global/classes/SectionAppearance";
+import type { DatabaseSectionAppearance } from "@/db-functions";
@@
- const [localAppearances, setLocalAppearances] = useState<
- SectionAppearance[]
- >([]);
+ const [localAppearances, setLocalAppearances] = useState<
+ DatabaseSectionAppearance[]
+ >([]);Also applies to: 62-64
🤖 Prompt for AI Agents
In apps/desktop/src/components/marcher/section/SectionAppearanceList.tsx around
line 22 (and also apply changes at lines 62-64), replace the runtime class type
SectionAppearance with the database shape type DatabaseSectionAppearance: import
the DatabaseSectionAppearance type from the module that declares your DB types
and change the local state and any variables/props typed as SectionAppearance to
DatabaseSectionAppearance so they match the snake_case query result shape;
update the type annotations for the state initialization and any map/filter
callbacks to use DatabaseSectionAppearance.
| const queryClient = useQueryClient(); | ||
| const { data: sectionAppearances } = useQuery( | ||
| allSectionAppearancesQueryOptions(), | ||
| ); | ||
| const { mutate: createSectionAppearances } = useMutation( | ||
| createSectionAppearancesMutationOptions(queryClient), | ||
| ); | ||
| const { mutate: updateSectionAppearances } = useMutation( | ||
| updateSectionAppearancesMutationOptions(queryClient), | ||
| ); | ||
| const { mutate: deleteSectionAppearances } = useMutation( | ||
| deleteSectionAppearancesMutationOptions(queryClient), | ||
| ); |
There was a problem hiding this comment.
Use mutateAsync and await; current try/catch and success toast run before the mutation finishes.
mutate is not promise-based; await mutate(...) resolves immediately and errors won't be caught. Switch to mutateAsync and await to sequence update/delete and correctly handle success/error.
Apply this diff:
const queryClient = useQueryClient();
const { data: sectionAppearances } = useQuery(
allSectionAppearancesQueryOptions(),
);
- const { mutate: createSectionAppearances } = useMutation(
+ const { mutateAsync: createSectionAppearancesAsync } = useMutation(
createSectionAppearancesMutationOptions(queryClient),
);
- const { mutate: updateSectionAppearances } = useMutation(
+ const { mutateAsync: updateSectionAppearancesAsync } = useMutation(
updateSectionAppearancesMutationOptions(queryClient),
);
- const { mutate: deleteSectionAppearances } = useMutation(
+ const { mutateAsync: deleteSectionAppearancesAsync } = useMutation(
deleteSectionAppearancesMutationOptions(queryClient),
);
@@
if (modifiedAppearances.length > 0) {
- updateSectionAppearances(modifiedAppearances);
+ await updateSectionAppearancesAsync(modifiedAppearances);
}
if (deletionsRef.current.length > 0) {
- deleteSectionAppearances(new Set(deletionsRef.current));
+ await deleteSectionAppearancesAsync(new Set(deletionsRef.current));
}
@@
try {
- await createSectionAppearances([newAppearance]);
+ await createSectionAppearancesAsync([newAppearance]);
toast.success(`Added style for ${sectionName}`);
} catch (error) {Also applies to: 96-103, 168-177
🤖 Prompt for AI Agents
In apps/desktop/src/components/marcher/section/SectionAppearanceList.tsx around
lines 49-61 (and likewise update usages at 96-103 and 168-177), the component
uses mutate (callback style) which is not promise-based so try/catch and success
toasts run before the mutation completes; replace mutate with mutateAsync, await
the call inside async handlers, and move try/catch around the awaited
mutateAsync so success and error toasts run after the mutation finishes; also
adjust handler signatures to be async and ensure you pass the same arguments to
mutateAsync as to mutate.
| let beatPosition = 0; | ||
| const measureStartBeatPositions: number[] = []; | ||
| const newBeats: NewBeatArgs[] = parsedMeasures.flatMap( | ||
| (measure) => { | ||
| measureStartBeatPositions.push(beatPosition); | ||
| return measure.beats.map((beat) => ({ | ||
| position: beatPosition++, | ||
| duration: beat.duration, | ||
| include_in_measure: true, | ||
| notes: beat.notes, | ||
| })); | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Fix type error: NewBeatArgs does not include position (likely build break).
You're passing position in NewBeatArgs objects. NewBeatArgs only allows duration/include_in_measure/notes. This is very likely the cause of the build failure.
Apply this diff to remove the invalid field and compute measure starts via an offset:
- let beatPosition = 0;
+ let beatIndex = 0;
const measureStartBeatPositions: number[] = [];
- const newBeats: NewBeatArgs[] = parsedMeasures.flatMap(
- (measure) => {
- measureStartBeatPositions.push(beatPosition);
- return measure.beats.map((beat) => ({
- position: beatPosition++,
- duration: beat.duration,
- include_in_measure: true,
- notes: beat.notes,
- }));
- },
- );
+ const newBeats: NewBeatArgs[] = parsedMeasures.flatMap(
+ (measure) => {
+ const start = beatIndex;
+ measureStartBeatPositions.push(start);
+ beatIndex += measure.beats.length;
+ return measure.beats.map((beat) => ({
+ duration: beat.duration,
+ include_in_measure: true,
+ notes: beat.notes,
+ }));
+ },
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let beatPosition = 0; | |
| const measureStartBeatPositions: number[] = []; | |
| const newBeats: NewBeatArgs[] = parsedMeasures.flatMap( | |
| (measure) => { | |
| measureStartBeatPositions.push(beatPosition); | |
| return measure.beats.map((beat) => ({ | |
| position: beatPosition++, | |
| duration: beat.duration, | |
| include_in_measure: true, | |
| notes: beat.notes, | |
| })); | |
| }, | |
| ); | |
| let beatIndex = 0; | |
| const measureStartBeatPositions: number[] = []; | |
| const newBeats: NewBeatArgs[] = parsedMeasures.flatMap( | |
| (measure) => { | |
| const start = beatIndex; | |
| measureStartBeatPositions.push(start); | |
| beatIndex += measure.beats.length; | |
| return measure.beats.map((beat) => ({ | |
| duration: beat.duration, | |
| include_in_measure: true, | |
| notes: beat.notes, | |
| })); | |
| }, | |
| ); |
🤖 Prompt for AI Agents
In apps/desktop/src/components/music/MusicXmlImport.ts around lines 98 to 110,
the NewBeatArgs objects currently include a non-existent `position` property
which causes a type/build error; remove `position` from the constructed objects
and instead compute measure start positions by tracking a running beat offset:
push the current offset into measureStartBeatPositions at the start of each
measure, create each beat object with only duration/include_in_measure/notes,
and increment the running beat offset by the beat's duration as you iterate (do
not place position on the NewBeatArgs).
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/desktop/electron/database/migrations/schema.ts (2)
186-193: Allow NULL path positions for marcher pages
path_start_position/path_end_positionare declared nullable and the docstring says NULL should be interpreted as 0/1, but this CHECK rejects NULL because SQLite treats NULL as constraint failure. Inserts will blow up whenever we leave these optional fields unset. Please relax the CHECK to tolerate NULL while still enforcing the 0‑1 bound when a value is provided, and regenerate the snapshot/migration after the change.check( "marcher_pages_path_data_position_check", - sql`path_start_position >= 0 AND path_start_position <= 1 AND path_end_position >= 0 AND path_end_position <= 1`, + sql`(path_start_position IS NULL OR (path_start_position >= 0 AND path_start_position <= 1)) AND (path_end_position IS NULL OR (path_end_position >= 0 AND path_end_position <= 1))`, ),
217-224: Allow NULL path positions for midsetsSame issue here: the CHECK currently bars NULL even though both columns are optional. This will cause failures when midsets don’t specify a path. Please apply the same NULL-tolerant guard as above.
check( "midsets_path_data_position_check", - sql`path_start_position >= 0 AND path_start_position <= 1 AND path_end_position >= 0 AND path_end_position <= 1`, + sql`(path_start_position IS NULL OR (path_start_position >= 0 AND path_start_position <= 1)) AND (path_end_position IS NULL OR (path_end_position >= 0 AND path_end_position <= 1))`, ),
🧹 Nitpick comments (1)
apps/desktop/src/__mocks__/TestSqlProxy.ts (1)
23-23: Keep initializeDatabase type-safeSwitching to
as anymasks the contract mismatch between the raw better-sqlite3 handle used in tests and the proxy/Drizzle database shapeinitializeDatabasenow expects. If the migrator starts calling proxy-only APIs (e.g.,batchor history helpers), this will compile but fail at runtime. Please feed the method the correctly typed database (wrap the in-memory handle the same way the renderer does or widen the accepted type inDrizzleMigrationService) instead of suppressing the type-checker.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
CLAUDE.md(0 hunks)apps/desktop/electron/database/__test__/DatabaseMocks.ts(1 hunks)apps/desktop/electron/database/database.services.ts(5 hunks)apps/desktop/electron/database/migrations/0008_round_mathemanic.sql(1 hunks)apps/desktop/electron/database/migrations/meta/0008_snapshot.json(1 hunks)apps/desktop/electron/database/migrations/meta/_journal.json(1 hunks)apps/desktop/electron/database/migrations/schema.ts(11 hunks)apps/desktop/electron/database/tables/__test__/testUtils.ts(2 hunks)apps/desktop/electron/preload/index.ts(2 hunks)apps/desktop/src/__mocks__/TestSqlProxy.ts(1 hunks)apps/desktop/src/__mocks__/components/ChangeMarchersSelection.mock.tsx(0 hunks)apps/desktop/src/__mocks__/globalMocks.ts(4 hunks)apps/desktop/src/components/canvas/__test__/MocksForCanvas.ts(3 hunks)
💤 Files with no reviewable changes (2)
- CLAUDE.md
- apps/desktop/src/mocks/components/ChangeMarchersSelection.mock.tsx
✅ Files skipped from review due to trivial changes (1)
- apps/desktop/electron/database/test/DatabaseMocks.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/desktop/electron/database/migrations/meta/_journal.json
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursor/rules/repo.mdc)
Use zustand for state management in the application
Files:
apps/desktop/electron/database/tables/__test__/testUtils.tsapps/desktop/electron/database/database.services.tsapps/desktop/electron/preload/index.tsapps/desktop/src/__mocks__/TestSqlProxy.tsapps/desktop/electron/database/migrations/schema.tsapps/desktop/src/__mocks__/globalMocks.tsapps/desktop/src/components/canvas/__test__/MocksForCanvas.ts
apps/desktop/electron/**/*.{ts,tsx,js}
📄 CodeRabbit inference engine (CLAUDE.md)
Use absolute paths for file operations; avoid relative file paths
Files:
apps/desktop/electron/database/tables/__test__/testUtils.tsapps/desktop/electron/database/database.services.tsapps/desktop/electron/preload/index.tsapps/desktop/electron/database/migrations/schema.ts
apps/desktop/electron/database/tables/**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Do not add or modify legacy IPC-based table files under electron/database/tables; they are being removed
Files:
apps/desktop/electron/database/tables/__test__/testUtils.ts
apps/desktop/electron/database/database.services.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Remove/avoid imports of legacy table modules from electron/database/database.services.ts
Files:
apps/desktop/electron/database/database.services.ts
apps/desktop/electron/preload/index.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Remove/avoid IPC handlers and calls for database access in electron/preload/index.ts
Files:
apps/desktop/electron/preload/index.ts
**/__mocks__/**
📄 CodeRabbit inference engine (CLAUDE.md)
Place test mocks under mocks directories
Files:
apps/desktop/src/__mocks__/TestSqlProxy.tsapps/desktop/src/__mocks__/globalMocks.ts
apps/desktop/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use the @/ alias for imports referencing the src/ directory
Files:
apps/desktop/src/__mocks__/TestSqlProxy.tsapps/desktop/src/__mocks__/globalMocks.tsapps/desktop/src/components/canvas/__test__/MocksForCanvas.ts
🧠 Learnings (17)
📓 Common learnings
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/database.services.ts : Remove/avoid imports of legacy table modules from electron/database/database.services.ts
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/**/*.ts : Do not add or modify legacy IPC-based table files under electron/database/tables; they are being removed
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/__test__/*.test.ts : Remove legacy tests under electron/database/tables/__test__; merge into classes tests
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/__test__/*.test.ts : Remove legacy tests under electron/database/tables/__test__; merge into classes tests
Applied to files:
apps/desktop/electron/database/tables/__test__/testUtils.tsapps/desktop/electron/database/database.services.tsapps/desktop/electron/preload/index.tsapps/desktop/src/__mocks__/TestSqlProxy.tsapps/desktop/electron/database/migrations/schema.tsapps/desktop/src/__mocks__/globalMocks.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/__test__/*.test.ts : In entity tests, call internal *Db functions for raw results and public wrappers for DatabaseResponse
Applied to files:
apps/desktop/electron/database/tables/__test__/testUtils.tsapps/desktop/src/__mocks__/TestSqlProxy.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/__test__/*.test.ts : Use setupTestSqlProxy() in beforeEach for DB-related tests
Applied to files:
apps/desktop/electron/database/tables/__test__/testUtils.tsapps/desktop/electron/database/database.services.tsapps/desktop/src/__mocks__/TestSqlProxy.ts
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Ensure full TypeScript type safety, including exported Database[TableName], New[TableName]Args, and Modified[TableName]Args types
Applied to files:
apps/desktop/electron/database/tables/__test__/testUtils.tsapps/desktop/electron/database/database.services.tsapps/desktop/src/__mocks__/TestSqlProxy.tsapps/desktop/src/__mocks__/globalMocks.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Use Drizzle-inferred types for DB entities (e.g., typeof table.$inferSelect) and avoid manually duplicating schema interfaces
Applied to files:
apps/desktop/electron/database/tables/__test__/testUtils.tsapps/desktop/src/__mocks__/TestSqlProxy.tsapps/desktop/electron/database/migrations/schema.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/__test__/*.test.ts : Consolidate tests for migrated entities under src/global/classes/__test__/[Entity].test.ts
Applied to files:
apps/desktop/electron/database/tables/__test__/testUtils.tsapps/desktop/src/__mocks__/globalMocks.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/database.services.ts : Remove/avoid imports of legacy table modules from electron/database/database.services.ts
Applied to files:
apps/desktop/electron/database/tables/__test__/testUtils.tsapps/desktop/electron/database/database.services.tsapps/desktop/electron/preload/index.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/__test__/*.test.ts : Ensure tests create required foreign key dependencies first
Applied to files:
apps/desktop/electron/database/tables/__test__/testUtils.tsapps/desktop/src/__mocks__/TestSqlProxy.ts
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/index.ts : Update apps/desktop/src/hooks/queries/index.ts to export the new hook (export * from "./useTableName")
Applied to files:
apps/desktop/electron/database/tables/__test__/testUtils.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/preload/index.ts : Remove/avoid IPC handlers and calls for database access in electron/preload/index.ts
Applied to files:
apps/desktop/electron/database/database.services.tsapps/desktop/electron/preload/index.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/**/*.ts : Do not add or modify legacy IPC-based table files under electron/database/tables; they are being removed
Applied to files:
apps/desktop/electron/database/database.services.tsapps/desktop/electron/preload/index.tsapps/desktop/electron/database/migrations/schema.tsapps/desktop/electron/database/migrations/meta/0008_snapshot.json
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Public wrapper functions should keep legacy names and wrap results in DatabaseResponse via promiseToDatabaseResponse
Applied to files:
apps/desktop/electron/database/database.services.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Internal database functions must use a *Db suffix and return raw data types (not DatabaseResponse)
Applied to files:
apps/desktop/electron/database/database.services.ts
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Integrate undo/redo by calling incrementUndoGroup within each transaction
Applied to files:
apps/desktop/electron/database/database.services.tsapps/desktop/electron/preload/index.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Prefer single transactions that group multi-operation DB work; use incrementUndoGroup within transactions
Applied to files:
apps/desktop/electron/database/database.services.ts
📚 Learning: 2025-08-11T21:28:23.647Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/unit-testing.mdc:0-0
Timestamp: 2025-08-11T21:28:23.647Z
Learning: Applies to **/test/**/*.{ts,tsx,js,jsx} : Do not use mocks for packages or database operations in tests; the only exception is mocking React context providers when rendering components that rely on hooks
Applied to files:
apps/desktop/src/__mocks__/TestSqlProxy.tsapps/desktop/src/__mocks__/globalMocks.ts
🧬 Code graph analysis (4)
apps/desktop/electron/database/tables/__test__/testUtils.ts (2)
apps/desktop/electron/database/db.ts (2)
schema(11-11)getOrm(7-9)apps/desktop/electron/database/services/DrizzleMigrationService.ts (1)
DrizzleMigrationService(19-151)
apps/desktop/electron/database/database.services.ts (1)
apps/desktop/src/global/database/db.ts (1)
db(6-17)
apps/desktop/src/__mocks__/globalMocks.ts (3)
apps/desktop/src/components/canvas/__test__/MocksForCanvas.ts (1)
mockMarchers(31-80)apps/desktop/src/db-functions/shapes.ts (1)
DatabaseShape(10-16)apps/desktop/src/db-functions/shapePages.ts (1)
DatabaseShapePage(23-31)
apps/desktop/src/components/canvas/__test__/MocksForCanvas.ts (4)
apps/desktop/src/__mocks__/globalMocks.ts (2)
mockMarcherPages(115-233)mockMarchers(13-62)apps/desktop/src/global/classes/MarcherPageIndex.ts (1)
marcherPageMapFromArray(34-54)apps/desktop/src/global/classes/MarcherVisualGroup.ts (1)
marcherVisualsFromMarchers(122-143)packages/core/src/field/FieldTheme.ts (1)
DEFAULT_FIELD_THEME(54-54)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: desktop-e2e (prerelease/0.0.13, pull-request)
- GitHub Check: build
- GitHub Check: desktop-e2e (main, main)
- GitHub Check: build-and-release (ubuntu-latest)
- GitHub Check: build-and-release (macos-13)
🔇 Additional comments (5)
apps/desktop/src/__mocks__/globalMocks.ts (2)
260-268: Give the last mockMarcherLine a unique ID.
mockMarcherLinesnow has two entries withid: 1, which trips consumers relying on unique identifiers. Bump the final ID to a fresh value.new MarcherLine({ - id: 1, + id: 3, startPageId: 2, endPageId: 4,
195-232: Ensure mockMarcherPages IDs are unique.The final three
mockMarcherPagesentries reuse IDs 1–3, overwriting the earlier pages when we build the marcherPageMap and breaking PK uniqueness assumptions. Please give those records their own IDs.{ - id: 1, + id: 7, marcher_id: 1, page_id: 3, @@ }, { - id: 2, + id: 8, marcher_id: 2, page_id: 3, @@ }, { - id: 3, + id: 9, marcher_id: 3, page_id: 3,apps/desktop/src/components/canvas/__test__/MocksForCanvas.ts (1)
7-7: Avoid pulling DEFAULT_FIELD_THEME through the package entry pointImporting
DEFAULT_FIELD_THEMEfrom@openmarch/corestill resolves topackages/core/dist/index.js, but the dist bundle isn’t produced as part of the test/build workflow, so this blows up in CI (same failure we already saw) and the canvas mocks can’t even load. Please keep the test fixture self-contained—e.g. provide a local mock theme or source it from a test helper that doesn’t hit the package entry point—or make sure the core package is built before tests run.apps/desktop/electron/database/database.services.ts (1)
193-201: Close DB handles in unsafe SQL proxyReiterating earlier feedback:
connect()opens a fresh handle every call. Without afinallythat closes it, repeated uses leak native connections.async function handleUnsafeSqlProxy(_: any, sql: string) { - try { - return await handleUnsafeSqlProxyWithDb(connect(), sql); + const db = connect(); + try { + return await handleUnsafeSqlProxyWithDb(db, sql); } catch (error: any) { console.error("Error from unsafe SQL proxy:", error); throw error; + } finally { + db.close(); } }apps/desktop/electron/preload/index.ts (1)
278-281: Guard renderer access to unsafeSqlProxyThis was previously flagged: keep the method but hard-block it unless
ENABLE_UNSAFE_SQL_PROXYis explicitly enabled, matching the main-process gate. Otherwise any renderer code (including plugins) can run arbitrary SQL.- /** Only needed for the triggers */ - unsafeSqlProxy: (sql: string) => - ipcRenderer.invoke("unsafeSql:proxy", sql) as Promise<{ - rows: any[] | any; - }>, + /** Only needed for the triggers */ + unsafeSqlProxy: (sql: string) => { + if (process.env.ENABLE_UNSAFE_SQL_PROXY !== "1") { + throw new Error("unsafeSqlProxy is disabled"); + } + return ipcRenderer.invoke("unsafeSql:proxy", sql) as Promise<{ + rows: any[] | any; + }>; + },
| async function handleUnsafeSqlProxyWithDb(db: Database.Database, sql: string) { | ||
| return await db.exec(sql); | ||
| } |
There was a problem hiding this comment.
Fix unsafe SQL proxy return payload
better-sqlite3#exec returns the Database instance, which can’t be cloned across the IPC boundary. Returning that object breaks the renderer call (and violates the { rows: … } contract we advertise downstream). Execute the SQL and respond with a serializable payload instead.
-async function handleUnsafeSqlProxyWithDb(db: Database.Database, sql: string) {
- return await db.exec(sql);
-}
+async function handleUnsafeSqlProxyWithDb(
+ db: Database.Database,
+ sql: string,
+) {
+ db.exec(sql);
+ return { rows: [] as any[] };
+}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/desktop/electron/database/database.services.ts around lines 156 to 158,
the function currently returns the better-sqlite3 Database instance (from
db.exec), which is non-serializable; change the handler to execute the SQL and
return a plain serializable payload: trim and inspect the SQL (e.g., startsWith
SELECT) and for queries use db.prepare(sql).all() and return { rows: [...] },
for non-query statements use db.prepare(sql).run() and return { changes:
<number>, lastInsertRowid: <number> } (or similar serializable fields) instead
of returning the Database object.
| ipcMain.handle("unsafeSql:proxy", handleUnsafeSqlProxy); | ||
|
|
There was a problem hiding this comment.
Gate unsafe SQL IPC registration
We shouldn’t register the unsafeSql:proxy channel in production builds. If ENABLE_UNSAFE_SQL_PROXY isn’t set, skip the handler so nothing (malicious or accidental) can invoke it.
export function initHandlers() {
// Generic SQL proxy handler for Drizzle ORM
ipcMain.handle("sql:proxy", handleSqlProxy);
- ipcMain.handle("unsafeSql:proxy", handleUnsafeSqlProxy);
+ if (process.env.ENABLE_UNSAFE_SQL_PROXY === "1") {
+ ipcMain.handle("unsafeSql:proxy", handleUnsafeSqlProxy);
+ }🤖 Prompt for AI Agents
In apps/desktop/electron/database/database.services.ts around lines 213-214, the
IPC handler for "unsafeSql:proxy" is always registered; modify the code to only
call ipcMain.handle("unsafeSql:proxy", handleUnsafeSqlProxy) when the
environment variable ENABLE_UNSAFE_SQL_PROXY is set/truthy (e.g.,
process.env.ENABLE_UNSAFE_SQL_PROXY === '1' or 'true') and the build is not
production if desired. Add a simple guard that checks the env var before
registering the handler and ensure no handler is registered when the flag is
absent, so the channel remains inaccessible in production builds.
| // create 16 beats | ||
| const beatValues = Array.from({ length: 16 }, (_, i) => ({ | ||
| duration: 0.5, | ||
| position: i + 1, | ||
| })); | ||
| void orm.insert(schema.beats).values(beatValues).run(); | ||
| return orm; |
There was a problem hiding this comment.
Await the seed insert to keep async drivers in sync.
DbConnection is typed as "async" and will back the sql.js proxy path you just introduced. With that driver, .run() returns a promise; dropping the result with void means initTestDatabaseOrm resolves before the beats are actually inserted, so downstream tests can race on missing seed data. Please await the insert so both async and sync drivers behave consistently.
- void orm.insert(schema.beats).values(beatValues).run();
+ await orm.insert(schema.beats).values(beatValues).run();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // create 16 beats | |
| const beatValues = Array.from({ length: 16 }, (_, i) => ({ | |
| duration: 0.5, | |
| position: i + 1, | |
| })); | |
| void orm.insert(schema.beats).values(beatValues).run(); | |
| return orm; | |
| // create 16 beats | |
| const beatValues = Array.from({ length: 16 }, (_, i) => ({ | |
| duration: 0.5, | |
| position: i + 1, | |
| })); | |
| await orm.insert(schema.beats).values(beatValues).run(); | |
| return orm; |
🤖 Prompt for AI Agents
In apps/desktop/electron/database/tables/__test__/testUtils.ts around lines 53
to 59, the seed insert uses void orm.insert(...).run() which drops the returned
promise and lets initTestDatabaseOrm resolve before the beats are actually
inserted; change this to await the insert call (await
orm.insert(schema.beats).values(beatValues).run()) so the function waits for the
async insert to complete and tests using async drivers (sql.js proxy) won't race
on missing seed data.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
apps/desktop/e2e/fixtures.mts (1)
17-34: Consider renaming to avoid shadowing.The new top-level
dbPathconstant now shares a name with the existingconst dbPath = …insideloadSqlIntoDatabase, which can be a little confusing when scanning the file. Renaming the top-level one to something likeblankDatabaseRelativePathwould keep intent obvious without changing behavior.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
.cursor/rules/basic-database-interaction.mdc(1 hunks).cursor/rules/component-database-interaction.mdc(1 hunks).eslintrc.cjs(3 hunks).github/workflows/test.yaml(1 hunks).vscode/launch.json(1 hunks).zed/.rules(3 hunks)apps/desktop/e2e/README.md(1 hunks)apps/desktop/e2e/_codegen.mjs(1 hunks)apps/desktop/e2e/fixtures.mts(2 hunks)apps/desktop/electron/database/DatabaseActions.ts(0 hunks)apps/desktop/electron/database/__test__/DatabaseActions.test.ts(0 hunks)apps/desktop/electron/database/migrations/0009_left_bullseye.sql(1 hunks)apps/desktop/electron/database/migrations/meta/0009_snapshot.json(1 hunks)apps/desktop/electron/database/migrations/meta/_journal.json(1 hunks)apps/desktop/electron/database/migrations/schema.ts(11 hunks)
💤 Files with no reviewable changes (2)
- apps/desktop/electron/database/DatabaseActions.ts
- apps/desktop/electron/database/test/DatabaseActions.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- .vscode/launch.json
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursor/rules/repo.mdc)
Use zustand for state management in the application
Files:
apps/desktop/electron/database/migrations/schema.ts
apps/desktop/electron/**/*.{ts,tsx,js}
📄 CodeRabbit inference engine (CLAUDE.md)
Use absolute paths for file operations; avoid relative file paths
Files:
apps/desktop/electron/database/migrations/schema.ts
🧠 Learnings (22)
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/__test__/*.test.ts : Remove legacy tests under electron/database/tables/__test__; merge into classes tests
Applied to files:
apps/desktop/e2e/_codegen.mjsapps/desktop/e2e/fixtures.mtsapps/desktop/electron/database/migrations/schema.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/database.services.ts : Remove/avoid imports of legacy table modules from electron/database/database.services.ts
Applied to files:
apps/desktop/e2e/_codegen.mjs
📚 Learning: 2025-08-11T21:28:23.647Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/unit-testing.mdc:0-0
Timestamp: 2025-08-11T21:28:23.647Z
Learning: Applies to **/test/**/*.{ts,tsx,js,jsx} : Do not use mocks for packages or database operations in tests; the only exception is mocking React context providers when rendering components that rely on hooks
Applied to files:
.eslintrc.cjs
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to **/*.test.ts : Test files must use the .test.ts suffix
Applied to files:
.eslintrc.cjs
📚 Learning: 2025-08-11T21:28:23.647Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/unit-testing.mdc:0-0
Timestamp: 2025-08-11T21:28:23.647Z
Learning: Applies to **/test/**/*.{ts,tsx,js,jsx} : Place tests in a test folder within the same directory as the file being tested
Applied to files:
.eslintrc.cjs
📚 Learning: 2025-08-11T21:28:23.647Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/unit-testing.mdc:0-0
Timestamp: 2025-08-11T21:28:23.647Z
Learning: Applies to **/test/**/*.{ts,tsx,js,jsx} : Use Vitest to write all unit tests
Applied to files:
.eslintrc.cjs
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to **/*.Table.ts : Do not create traditional *.Table.ts files
Applied to files:
.eslintrc.cjs
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Implement smart cache invalidation: invalidate affected detail queries and list queries after mutations
Applied to files:
.zed/.rules
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Ensure full TypeScript type safety, including exported Database[TableName], New[TableName]Args, and Modified[TableName]Args types
Applied to files:
.zed/.rules.cursor/rules/basic-database-interaction.mdc.cursor/rules/component-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/__test__/*.test.ts : Use setupTestSqlProxy() in beforeEach for DB-related tests
Applied to files:
apps/desktop/e2e/README.md
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Always use React Query hooks for all new database table interactions instead of traditional table files
Applied to files:
.cursor/rules/basic-database-interaction.mdc.cursor/rules/component-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Follow naming conventions in hook files: file use[TableName].ts; hooks use[TableName], use[TableName]ById, useCreate[TableName], etc.; query keys [tableName] (camelCase); types Database[TableName], New[TableName]Args, Modified[TableName]Args
Applied to files:
.cursor/rules/basic-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Wrap all mutations in database transactions
Applied to files:
.cursor/rules/basic-database-interaction.mdc.cursor/rules/component-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Create new query hook files at apps/desktop/src/hooks/queries/use[TableName].ts for each table
Applied to files:
.cursor/rules/basic-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Always export hooks: use[TableName], use[TableName]ById, useCreate[TableName], useUpdate[TableName], useDelete[TableName]
Applied to files:
.cursor/rules/basic-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/index.ts : Update apps/desktop/src/hooks/queries/index.ts to export the new hook (export * from "./useTableName")
Applied to files:
.cursor/rules/basic-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Leverage React Query error and loading states; do not implement manual useState for loading/error management
Applied to files:
.cursor/rules/basic-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/**/*.ts : Do not add or modify legacy IPC-based table files under electron/database/tables; they are being removed
Applied to files:
apps/desktop/e2e/fixtures.mtsapps/desktop/electron/database/migrations/schema.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Prefer single transactions that group multi-operation DB work; use incrementUndoGroup within transactions
Applied to files:
.cursor/rules/component-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Integrate undo/redo by calling incrementUndoGroup within each transaction
Applied to files:
.cursor/rules/component-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Consolidate each entity’s database logic into a single file src/global/classes/[Entity].ts (shared SQL bridge)
Applied to files:
apps/desktop/electron/database/migrations/schema.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Use Drizzle-inferred types for DB entities (e.g., typeof table.$inferSelect) and avoid manually duplicating schema interfaces
Applied to files:
apps/desktop/electron/database/migrations/schema.ts
🧬 Code graph analysis (1)
apps/desktop/e2e/fixtures.mts (1)
apps/desktop/e2e/_codegen.mjs (2)
dbPath(79-79)__dirname(10-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: build-and-release (macos-13)
- GitHub Check: build-and-release (ubuntu-latest)
- GitHub Check: build-and-release (macos-latest)
- GitHub Check: desktop-e2e (prerelease/0.0.13, pull-request)
- GitHub Check: build
🔇 Additional comments (6)
apps/desktop/e2e/README.md (1)
26-26: Fixture reference update looks good.
Renaming the base fixture to_blank.dotskeeps the README aligned with the current setup script..zed/.rules (1)
170-174: Void prefix silences promise lint cleanly.Nice touch adding
voidbefore eachinvalidateQueriescall; it keeps the callbacks fire-and-forget while satisfying the no-floating-promises rule without altering behavior.Also applies to: 206-214, 239-247
apps/desktop/e2e/_codegen.mjs (1)
79-84: Scoped path constant looks good.Centralizing the relative
_blank.dotspath intodbPathmakes the error message clearer without changing behavior. 👍.eslintrc.cjs (1)
3-8: Install the TanStack Query ESLint plugin wherever linting runs.Extending
plugin:@tanstack/query/recommendedrequires@tanstack/eslint-plugin-queryto be present; ESLint throws “Failed to load config” if the package is missing. Please add it (e.g., as a devDependency in the root/workspaces that lint) or confirm it’s already installed. Docs explicitly call for installing the plugin before using this preset.#!/bin/bash # Verify every package.json that runs ESLint declares the plugin dependency. set -euo pipefail echo "Packages missing @tanstack/eslint-plugin-query:" fd -H package.json | while read -r pkg; do found=$(jq -r 'paths | select(length==2 and (.[0]=="devDependencies" or .[0]=="dependencies") and .[-1]=="@tanstack/eslint-plugin-query") | empty' "$pkg") if [[ -z "$found" ]]; then echo " - $pkg" fi done.cursor/rules/basic-database-interaction.mdc (2)
197-205: Remove.get()after.returning()in Drizzle SQLite.Drizzle’s
.returning()already yields an array of rows;.get()does not exist here and following this template will throw at runtime. Destructure the array (e.g.,const [updatedItem] = …returning();) before pushing intoupdatedItems.
348-356: Don’t pass an array of keys toqueryKeyininvalidateQueries.
invalidateQueriesexpects a single query key. This template hands it an array of query keys (Array.from(itemIds).map(...)), which React Query v5 rejects. Loop the IDs and invalidate each key individually (or use a predicate).
| import { | ||
| DbConnection, | ||
| Database{tableName}, | ||
| create{tableName}s, | ||
| get{tableName}s, | ||
| get{tableName}ById, | ||
| realDatabase{tableName}ToDatabase{tableName}, | ||
| delete{tableName}s, | ||
| update{tableName}sInTransaction, | ||
| } from "@/db-functions"; | ||
|
|
||
| const { table_name } = schema; | ||
|
|
||
| const KEY_BASE = "{table_name}"; | ||
|
|
||
| // Query key factory | ||
| export const {tableName}Keys = { | ||
| all: () => [KEY_BASE] as const, | ||
| byId: (id: number) => [KEY_BASE, "id", id] as const, | ||
| // Add other specific keys as needed | ||
| }; | ||
|
|
||
| const {tableName}Queries = { | ||
| getAll: async (db: DbConnection): Promise<Database{tableName}[]> => { | ||
| return await get{tableName}s({ db }); | ||
| }, | ||
| getById: async ( | ||
| db: DbConnection, | ||
| id: number, | ||
| ): Promise<Database{tableName} | undefined> => { | ||
| return await get{tableName}ById({ db, id }); | ||
| }, | ||
| }; | ||
|
|
||
| /** | ||
| * Query options for getting all items | ||
| */ | ||
| export const all{tableName}sQueryOptions = () => { | ||
| return queryOptions<Database{tableName}[]>({ | ||
| queryKey: {tableName}Keys.all(), | ||
| queryFn: async () => { | ||
| return await {tableName}Queries.getAll(db); | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| export const {tableName}QueryByIdOptions = (id: number) => { | ||
| return queryOptions<Database{tableName} | undefined>({ | ||
| queryKey: {tableName}Keys.byId(id), | ||
| queryFn: async () => { | ||
| return await {tableName}Queries.getById(db, id); | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| export const create{tableName}sMutationOptions = (qc: QueryClient) => { | ||
| return mutationOptions({ | ||
| mutationFn: (newItems: New{tableName}Args[]) => | ||
| create{tableName}s({ db, newItems }), | ||
| onSuccess: (_, variables) => { | ||
| // Invalidate all queries | ||
| void qc.invalidateQueries({ | ||
| queryKey: [KEY_BASE], | ||
| }); | ||
| }, | ||
| onError: (e, variables) => { | ||
| conToastError(`Error creating items`, e, variables); | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| export const update{tableName}sMutationOptions = (qc: QueryClient) => { | ||
| return mutationOptions({ | ||
| mutationFn: (modifiedItems: Modified{tableName}Args[]) => | ||
| update{tableName}s({ db, modifiedItems }), | ||
| onSuccess: (_, variables) => { |
There was a problem hiding this comment.
Import the function you call in the mutation template.
The hooks template calls update{tableName}s({ db, modifiedItems }), but the import list omits update{tableName}s. Anyone following this pattern will hit a TS compile error. Add update{tableName}s to the imports.
🤖 Prompt for AI Agents
In .cursor/rules/basic-database-interaction.mdc around lines 268 to 343 the
template calls update{tableName}s(...) in the mutation but that function is
missing from the import list at the top; add update{tableName}s to the imported
identifiers from "@/db-functions" so the mutation call compiles, keeping the
same import ordering/style as the surrounding imports.
| mutationFn, | ||
| onSuccess: () => { | ||
| // Invalidate all of the relevant queries | ||
| // Always invalidate beats first and wait for them to finish. This is important because measures rely on beats | ||
| await queryClient.invalidateQueries({ | ||
| queryKey: beatKeys.all(), | ||
| }); | ||
| void queryClient.invalidateQueries({ | ||
| queryKey: measureKeys.all(), | ||
| }); | ||
| // ... more queries if needed | ||
|
|
||
| if (successKey) toast.success(tolgee.t(successKey)); | ||
| }, | ||
| onError: (error) => { | ||
| conToastError(tolgee.t(errorKey), error); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Make the onSuccess handler async before using await.
The example calls await queryClient.invalidateQueries(...) inside onSuccess, but the handler isn’t marked async, so the snippet is invalid as written. Update the template to onSuccess: async () => { … }.
🤖 Prompt for AI Agents
In .cursor/rules/component-database-interaction.mdc around lines 96 to 113 the
onSuccess handler uses await but isn’t declared async; change the handler
signature to onSuccess: async () => { … } so await is valid, leaving the rest of
the logic (invalidateQueries calls and toast) unchanged.
| strategy: | ||
| matrix: | ||
| include: | ||
| - branch: main | ||
| artifact: main | ||
| - branch: ${{ github.head_ref }} | ||
| artifact: pull-request | ||
|
|
There was a problem hiding this comment.
Avoid redundant matrix entries
The matrix defines branch/artifact, but none of the steps consume those values. Each matrix run just checks out the default PR merge commit, so the workflow executes twice with identical inputs, doubling runtime without adding coverage. Either drop the matrix or wire matrix.branch into the checkout (and any other intended branch-specific logic).
🤖 Prompt for AI Agents
.github/workflows/test.yaml lines 10-17: The matrix currently includes
branch/artifact pairs that are never used, causing duplicate identical runs;
either remove the entire strategy.matrix block to run once, or wire the matrix
values into downstream steps — update actions/checkout to use ref: ${{
matrix.branch }} (or otherwise pass matrix.branch into any branch-specific
steps) and reference matrix.artifact where artifacts are created/uploaded so
each matrix entry produces distinct behavior.
| const timestamps = { | ||
| created_at: text() | ||
| .notNull() | ||
| .default(sql`(CURRENT_TIMESTAMP)`), | ||
| updated_at: text() | ||
| .notNull() | ||
| .default(sql`(CURRENT_TIMESTAMP)`) | ||
| .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`), | ||
| }; |
There was a problem hiding this comment.
Turn the shared timestamps helper into a factory before reuse
timestamps currently holds live column-builder instances. Drizzle’s builders are single-use; spreading the same builder across multiple sqliteTable calls trips Drizzle’s guard (ColumnBuilder was already used) and prevents the module from loading. Convert the helper into a function that returns fresh builders for each table, and call it where needed.
-const timestamps = {
- created_at: text()
- .notNull()
- .default(sql`(CURRENT_TIMESTAMP)`),
- updated_at: text()
- .notNull()
- .default(sql`(CURRENT_TIMESTAMP)`)
- .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
-};
+const timestamps = () => ({
+ created_at: text()
+ .notNull()
+ .default(sql`(CURRENT_TIMESTAMP)`),
+ updated_at: text()
+ .notNull()
+ .default(sql`(CURRENT_TIMESTAMP)`)
+ .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
+});Then spread ...timestamps() in each table definition.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const timestamps = { | |
| created_at: text() | |
| .notNull() | |
| .default(sql`(CURRENT_TIMESTAMP)`), | |
| updated_at: text() | |
| .notNull() | |
| .default(sql`(CURRENT_TIMESTAMP)`) | |
| .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`), | |
| }; | |
| const timestamps = () => ({ | |
| created_at: text() | |
| .notNull() | |
| .default(sql`(CURRENT_TIMESTAMP)`), | |
| updated_at: text() | |
| .notNull() | |
| .default(sql`(CURRENT_TIMESTAMP)`) | |
| .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`), | |
| }); |
🤖 Prompt for AI Agents
In apps/desktop/electron/database/migrations/schema.ts around lines 14 to 22,
the shared timestamps object currently holds live Drizzle column-builder
instances which are single-use and cause "ColumnBuilder was already used" when
reused; convert that const into a factory function (e.g., function timestamps()
{ return { created_at: text().notNull().default(sql`(CURRENT_TIMESTAMP)`),
updated_at: text().notNull().default(sql`(CURRENT_TIMESTAMP)`).$onUpdate(() =>
sql`(CURRENT_TIMESTAMP)`) }; }) so each call returns fresh builders, and update
all table definitions to spread the result with ...timestamps() instead of
spreading the original object.
Need to wait on a promise to complete make sure the transaction completes
The metronome tests would generate audio files, and we don't want these in git history
Move fixtures to test root clean up exports remove absolute path
Add test driver for better-sqlite and sqljs
Re-structure queries so that they are called with options rather than the hook itself
Beats must be invalidated first
[BUG]: Edit notes on the first page fix #533
Blank file is now updated and migrated E2E tests pass
ensure history group is 1-based
693bce1 to
7a42b0e
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/desktop/electron/database/migrations/schema.ts (1)
58-77: Add unique constraint for beats.position to match intent.Comment says “Integer and unique,” but constraint is missing. Add a unique index.
export const beats = sqliteTable( "beats", { id: integer().primaryKey(), /** Duration from this beat to the next in second. */ duration: real().notNull(), /** The position of this beat in the show. Integer and unique */ position: integer().notNull(), /** Whether this beat is included in a measure. 0 = false, 1 = true. */ include_in_measure: integer().default(1).notNull(), /** Human readable notes. */ notes: text(), - ...timestamps, + ...timestamps(), }, - (_table) => [ + (table) => [ check("beats_duration_check", sql`duration >= 0`), check("beats_position_check", sql`position >= 0`), check("beats_include_in_measure", sql`include_in_measure IN (0, 1)`), + unique().on(table.position), ], );
🧹 Nitpick comments (7)
apps/desktop/electron/database/services/sqljs-driver.ts (2)
14-16: Prefer the publicSQLJsDatabasetype export to avoid mismatches.Self-typing
BaseSQLiteDatabase<"sync", void, TSchema>can diverge from Drizzle’s actualRunResulttype. Import/re-exportSQLJsDatabasefromdrizzle-orm/sql-jsinstead.
32-43: Avoid duplicating relational schema extraction.Manually calling
extractTablesRelationalConfigmirrors Drizzle internals and can drift. The publicdrizzlealready derives this fromconfig.schema. Drop this block if you switch to the public adapter.apps/desktop/electron/database/services/DrizzleMigrationService.ts (2)
107-134: Use absolute paths for file IO (packaging-safe).hasPendingMigrations defaults to a relative path. Resolve from __dirname to comply with the guideline and avoid CWD issues.
As per coding guidelines.
- const folder = migrationsFolder || "./electron/database/migrations"; + const folder = + migrationsFolder || + path.resolve(__dirname, "../migrations");
148-149: Make trigger creation idempotent or drop-before-create.createAllTriggers will fail on existing triggers unless the SQL uses IF NOT EXISTS. Either prepend a dropAllTriggers call here or update trigger DDL to be idempotent.
apps/desktop/electron/database/migrations/schema.ts (2)
24-33: Make browserSafeBinaryBlob null- and Buffer-safe.Handle nulls and Buffer/ArrayBuffer inputs to avoid runtime errors when the column is nullable.
-const browserSafeBinaryBlob = customType<{ - data: Uint8Array; - driverData: Uint8Array; -}>({ +const browserSafeBinaryBlob = customType<{ + data: Uint8Array | null; + driverData: Uint8Array | null; +}>({ dataType: () => "blob", - fromDriver: (value) => new Uint8Array(value), - toDriver: (value) => value, + fromDriver: (value) => + value == null + ? null + : value instanceof Uint8Array + ? value + : new Uint8Array(value as ArrayBuffer | Buffer), + toDriver: (value) => value, });
240-247: Use browserSafeBinaryBlob for audio_files.data for web portability.blob() infers Buffer in Node; switch to the Uint8Array-backed custom type (as with field_properties.image) to avoid type divergence in the renderer/SQL.js.
export const audio_files = sqliteTable("audio_files", { id: integer().primaryKey(), path: text().notNull(), nickname: text(), - data: blob(), + data: browserSafeBinaryBlob(), selected: integer().default(0).notNull(), - ...timestamps, + ...timestamps(), });apps/desktop/electron/database/migrations/0007_misty_zemo.sql (1)
16-31: Add unique index for beats.position here (optional if added in a later migration).To enforce the intended uniqueness:
-- After renaming __new_beats to beats: CREATE UNIQUE INDEX IF NOT EXISTS beats_position_unique ON beats (position);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
packages/path-utility/example/public/vite.svgis excluded by!**/*.svgpackages/path-utility/example/src/assets/react.svgis excluded by!**/*.svg
📒 Files selected for processing (46)
.cursor/rules/basic-database-interaction.mdc(1 hunks).cursor/rules/command-syntax.mdc(1 hunks).cursor/rules/component-database-interaction.mdc(1 hunks).cursor/rules/database-interaction.mdc(0 hunks).cursor/rules/multi-query-hooks.mdc(1 hunks).cursor/rules/unit-testing.mdc(1 hunks).eslintrc.cjs(3 hunks).github/workflows/test.yaml(1 hunks).gitignore(1 hunks).vscode/launch.json(1 hunks).zed/.rules(3 hunks)CLAUDE.md(0 hunks)apps/desktop/e2e/README.md(1 hunks)apps/desktop/e2e/_codegen.mjs(1 hunks)apps/desktop/e2e/fixtures.mts(2 hunks)apps/desktop/electron/database/DatabaseActions.ts(0 hunks)apps/desktop/electron/database/__test__/DatabaseActions.test.ts(0 hunks)apps/desktop/electron/database/__test__/DatabaseMocks.ts(1 hunks)apps/desktop/electron/database/__test__/database.history.test.ts(0 hunks)apps/desktop/electron/database/constants.ts(0 hunks)apps/desktop/electron/database/database.services.ts(5 hunks)apps/desktop/electron/database/migrations/0000_productive_madripoor.sql(1 hunks)apps/desktop/electron/database/migrations/0006_bitter_proemial_gods.sql(1 hunks)apps/desktop/electron/database/migrations/0007_misty_zemo.sql(1 hunks)apps/desktop/electron/database/migrations/0008_round_mathemanic.sql(1 hunks)apps/desktop/electron/database/migrations/0009_left_bullseye.sql(1 hunks)apps/desktop/electron/database/migrations/meta/0006_snapshot.json(1 hunks)apps/desktop/electron/database/migrations/meta/0007_snapshot.json(1 hunks)apps/desktop/electron/database/migrations/meta/0008_snapshot.json(1 hunks)apps/desktop/electron/database/migrations/meta/0009_snapshot.json(1 hunks)apps/desktop/electron/database/migrations/meta/_journal.json(1 hunks)apps/desktop/electron/database/migrations/schema.ts(11 hunks)apps/desktop/electron/database/migrations/triggers.ts(3 hunks)apps/desktop/electron/database/services/DrizzleMigrationService.ts(3 hunks)apps/desktop/electron/database/services/sqljs-driver.ts(1 hunks)apps/desktop/electron/database/tables/BeatTable.ts(0 hunks)apps/desktop/electron/database/tables/MarcherPageTable.ts(0 hunks)apps/desktop/electron/database/tables/MarcherTable.ts(0 hunks)apps/desktop/electron/database/tables/PageTable.ts(0 hunks)apps/desktop/electron/database/tables/ShapePageMarcherTable.ts(0 hunks)apps/desktop/electron/database/tables/ShapePageTable.ts(0 hunks)apps/desktop/electron/database/tables/ShapeTable.ts(0 hunks)apps/desktop/electron/database/tables/UtilityTable.ts(0 hunks)apps/desktop/electron/database/tables/__test__/BeatTable.test.ts(0 hunks)apps/desktop/electron/database/tables/__test__/MarcherPageTable.test.ts(0 hunks)apps/desktop/electron/database/tables/__test__/MarcherTable.test.ts(0 hunks)
💤 Files with no reviewable changes (17)
- apps/desktop/electron/database/tables/PageTable.ts
- apps/desktop/electron/database/tables/test/MarcherTable.test.ts
- apps/desktop/electron/database/DatabaseActions.ts
- apps/desktop/electron/database/test/database.history.test.ts
- apps/desktop/electron/database/constants.ts
- CLAUDE.md
- apps/desktop/electron/database/tables/ShapeTable.ts
- apps/desktop/electron/database/tables/MarcherPageTable.ts
- apps/desktop/electron/database/test/DatabaseActions.test.ts
- apps/desktop/electron/database/tables/test/MarcherPageTable.test.ts
- .cursor/rules/database-interaction.mdc
- apps/desktop/electron/database/tables/UtilityTable.ts
- apps/desktop/electron/database/tables/BeatTable.ts
- apps/desktop/electron/database/tables/MarcherTable.ts
- apps/desktop/electron/database/tables/ShapePageTable.ts
- apps/desktop/electron/database/tables/ShapePageMarcherTable.ts
- apps/desktop/electron/database/tables/test/BeatTable.test.ts
✅ Files skipped from review due to trivial changes (1)
- .cursor/rules/component-database-interaction.mdc
🚧 Files skipped from review as they are similar to previous changes (13)
- apps/desktop/e2e/fixtures.mts
- .cursor/rules/unit-testing.mdc
- apps/desktop/electron/database/migrations/meta/0006_snapshot.json
- .vscode/launch.json
- .zed/.rules
- .github/workflows/test.yaml
- apps/desktop/electron/database/migrations/meta/0008_snapshot.json
- apps/desktop/electron/database/migrations/0000_productive_madripoor.sql
- .cursor/rules/multi-query-hooks.mdc
- apps/desktop/electron/database/migrations/meta/_journal.json
- apps/desktop/electron/database/test/DatabaseMocks.ts
- .cursor/rules/basic-database-interaction.mdc
- apps/desktop/e2e/_codegen.mjs
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursor/rules/repo.mdc)
Use zustand for state management in the application
Files:
apps/desktop/electron/database/services/sqljs-driver.tsapps/desktop/electron/database/database.services.tsapps/desktop/electron/database/migrations/triggers.tsapps/desktop/electron/database/migrations/schema.tsapps/desktop/electron/database/services/DrizzleMigrationService.ts
apps/desktop/electron/**/*.{ts,tsx,js}
📄 CodeRabbit inference engine (CLAUDE.md)
Use absolute paths for file operations; avoid relative file paths
Files:
apps/desktop/electron/database/services/sqljs-driver.tsapps/desktop/electron/database/database.services.tsapps/desktop/electron/database/migrations/triggers.tsapps/desktop/electron/database/migrations/schema.tsapps/desktop/electron/database/services/DrizzleMigrationService.ts
apps/desktop/electron/database/database.services.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Remove/avoid imports of legacy table modules from electron/database/database.services.ts
Files:
apps/desktop/electron/database/database.services.ts
🧠 Learnings (22)
📓 Common learnings
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/__test__/*.test.ts : Remove legacy tests under electron/database/tables/__test__; merge into classes tests
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Always use React Query hooks for all new database table interactions instead of traditional table files
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/__test__/*.test.ts : Use setupTestSqlProxy() in beforeEach for DB-related tests
Applied to files:
apps/desktop/e2e/README.mdapps/desktop/electron/database/database.services.tsapps/desktop/electron/database/migrations/triggers.tsapps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Use Drizzle-inferred types for DB entities (e.g., typeof table.$inferSelect) and avoid manually duplicating schema interfaces
Applied to files:
apps/desktop/electron/database/services/sqljs-driver.tsapps/desktop/electron/database/migrations/schema.tsapps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Consolidate each entity’s database logic into a single file src/global/classes/[Entity].ts (shared SQL bridge)
Applied to files:
apps/desktop/electron/database/services/sqljs-driver.tsapps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/__test__/*.test.ts : Remove legacy tests under electron/database/tables/__test__; merge into classes tests
Applied to files:
apps/desktop/electron/database/services/sqljs-driver.tsapps/desktop/electron/database/database.services.tsapps/desktop/electron/database/migrations/triggers.tsapps/desktop/electron/database/migrations/schema.tsapps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Run pnpm tsc --noEmit for type checking and pnpm lint for linting in the desktop app
Applied to files:
apps/desktop/electron/database/services/sqljs-driver.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/database.services.ts : Remove/avoid imports of legacy table modules from electron/database/database.services.ts
Applied to files:
apps/desktop/electron/database/database.services.tsapps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/preload/index.ts : Remove/avoid IPC handlers and calls for database access in electron/preload/index.ts
Applied to files:
apps/desktop/electron/database/database.services.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/**/*.ts : Do not add or modify legacy IPC-based table files under electron/database/tables; they are being removed
Applied to files:
apps/desktop/electron/database/database.services.tsapps/desktop/electron/database/migrations/schema.tsapps/desktop/electron/database/services/DrizzleMigrationService.tsapps/desktop/electron/database/migrations/meta/0007_snapshot.json
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Ensure full TypeScript type safety, including exported Database[TableName], New[TableName]Args, and Modified[TableName]Args types
Applied to files:
apps/desktop/electron/database/database.services.tsapps/desktop/electron/database/migrations/triggers.tsapps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Public wrapper functions should keep legacy names and wrap results in DatabaseResponse via promiseToDatabaseResponse
Applied to files:
apps/desktop/electron/database/database.services.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Internal database functions must use a *Db suffix and return raw data types (not DatabaseResponse)
Applied to files:
apps/desktop/electron/database/database.services.tsapps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Integrate undo/redo by calling incrementUndoGroup within each transaction
Applied to files:
apps/desktop/electron/database/database.services.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Prefer single transactions that group multi-operation DB work; use incrementUndoGroup within transactions
Applied to files:
apps/desktop/electron/database/database.services.tsapps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-11T21:28:23.647Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/unit-testing.mdc:0-0
Timestamp: 2025-08-11T21:28:23.647Z
Learning: Applies to **/test/**/*.{ts,tsx,js,jsx} : Do not use mocks for packages or database operations in tests; the only exception is mocking React context providers when rendering components that rely on hooks
Applied to files:
.eslintrc.cjs
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to **/*.test.ts : Test files must use the .test.ts suffix
Applied to files:
.eslintrc.cjs
📚 Learning: 2025-08-11T21:28:23.647Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/unit-testing.mdc:0-0
Timestamp: 2025-08-11T21:28:23.647Z
Learning: Applies to **/test/**/*.{ts,tsx,js,jsx} : Place tests in a test folder within the same directory as the file being tested
Applied to files:
.eslintrc.cjs
📚 Learning: 2025-08-11T21:28:23.647Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/unit-testing.mdc:0-0
Timestamp: 2025-08-11T21:28:23.647Z
Learning: Applies to **/test/**/*.{ts,tsx,js,jsx} : Use Vitest to write all unit tests
Applied to files:
.eslintrc.cjs
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to **/*.Table.ts : Do not create traditional *.Table.ts files
Applied to files:
.eslintrc.cjs
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/__test__/*.test.ts : In entity tests, call internal *Db functions for raw results and public wrappers for DatabaseResponse
Applied to files:
apps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/__test__/*.test.ts : Consolidate tests for migrated entities under src/global/classes/__test__/[Entity].test.ts
Applied to files:
apps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Wrap all mutations in database transactions
Applied to files:
apps/desktop/electron/database/services/DrizzleMigrationService.ts
🧬 Code graph analysis (3)
apps/desktop/electron/database/database.services.ts (1)
apps/desktop/src/global/database/db.ts (1)
db(6-17)
apps/desktop/electron/database/migrations/triggers.ts (2)
apps/desktop/electron/database/tables/__test__/testUtils.ts (1)
DbConnection(8-12)apps/desktop/electron/database/services/DrizzleMigrationService.ts (1)
DB(13-13)
apps/desktop/electron/database/services/DrizzleMigrationService.ts (3)
apps/desktop/electron/database/db.ts (2)
DB(5-5)schema(11-11)apps/desktop/electron/database/tables/__test__/testUtils.ts (1)
DbConnection(8-12)apps/desktop/electron/database/migrations/triggers.ts (2)
dropAllTriggers(60-69)createAllTriggers(75-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: test (prerelease/0.0.13, pull-request)
- GitHub Check: test (main, main)
- GitHub Check: build-and-release (macos-13)
- GitHub Check: build-and-release (ubuntu-latest)
- GitHub Check: build
- GitHub Check: desktop-e2e (prerelease/0.0.13, pull-request)
- GitHub Check: desktop-e2e (main, main)
🔇 Additional comments (14)
apps/desktop/e2e/README.md (1)
26-26: Fixture name correction looks goodUpdating the docs to point at
_blank.dotskeeps the instructions aligned with the current fixture naming—thanks for catching it..gitignore (1)
48-50: Do not ignore the entire metronome package.We still have tracked sources under
packages/metronome, so this glob will hide real code and block future changes from being committed. Please drop it or replace it with a root-anchored ignore that targets only generated artifacts..eslintrc.cjs (1)
3-8: Install@tanstack/eslint-plugin-querywhere ESLint runs.Extending
plugin:@tanstack/query/recommendedwill error unless@tanstack/eslint-plugin-queryis listed in the workspaces that run ESLint (root or per-package). The last repo scan still showed it missing in several package.json files, so lint will break. Please add the dependency and re-run installs, or confirm it’s already in place.Run this script to re-check the tree:
#!/bin/bash set -euo pipefail echo "Packages missing @tanstack/eslint-plugin-query:" fd -a package.json | while read -r f; do has=$(jq -r '(.devDependencies["@tanstack/eslint-plugin-query"] // .dependencies["@tanstack/eslint-plugin-query"]) // empty' "$f") if [[ -z "$has" ]]; then echo " - $f" fi doneapps/desktop/electron/database/services/sqljs-driver.ts (1)
25-30: Type the logger and narrow correctly (fixes strict TS build).
let logger;is implicitanyunder noImplicitAny, andconfig.logger !== falseassignsundefined. Type it and guard on object.-import { DefaultLogger } from "drizzle-orm/logger"; +import { DefaultLogger, type Logger } from "drizzle-orm/logger"; @@ - let logger; + let logger: Logger | undefined; if (config.logger === true) { logger = new DefaultLogger(); - } else if (config.logger !== false) { - logger = config.logger; + } else if (typeof config.logger === "object") { + logger = config.logger; }apps/desktop/electron/database/database.services.ts (3)
156-158: Return a serializable payload from the unsafe proxy.
better-sqlite3#execreturns theDatabaseobject, which cannot cross the IPC boundary and breaks the renderer contract. Execute the SQL and return data/plain objects instead of the Database instance.Apply this diff:
async function handleUnsafeSqlProxyWithDb( db: Database.Database, sql: string, ) { - return await db.exec(sql); + const trimmed = sql.trim(); + if (/^select/i.test(trimmed)) { + const rows = db.prepare(sql).all(); + return { rows }; + } + const result = db.prepare(sql).run(); + return { + rows: [], + changes: result.changes, + lastInsertRowid: + typeof result.lastInsertRowid === "bigint" + ? Number(result.lastInsertRowid) + : result.lastInsertRowid, + }; }
194-201: Close the temporary DB connection inhandleUnsafeSqlProxy.Each invocation opens a new
better-sqlite3connection and never closes it, leaking handles and eventually exhausting resources. Reuse the connection or at least close it in afinally.Apply this diff:
async function handleUnsafeSqlProxy(_: any, sql: string) { - try { - return await handleUnsafeSqlProxyWithDb(connect(), sql); + const db = connect(); + try { + return await handleUnsafeSqlProxyWithDb(db, sql); } catch (error: any) { console.error("Error from unsafe SQL proxy:", error); throw error; + } finally { + db.close(); } }
211-214: Guard unsafe SQL IPC registration behind an env flag.The
unsafeSql:proxychannel should only exist when explicitly enabled (e.g., for local debugging). Registering it unconditionally exposes a production attack surface.Apply this diff:
export function initHandlers() { // Generic SQL proxy handler for Drizzle ORM ipcMain.handle("sql:proxy", handleSqlProxy); - ipcMain.handle("unsafeSql:proxy", handleUnsafeSqlProxy); + if (process.env.ENABLE_UNSAFE_SQL_PROXY === "1") { + ipcMain.handle("unsafeSql:proxy", handleUnsafeSqlProxy); + }apps/desktop/electron/database/services/DrizzleMigrationService.ts (3)
10-13: Move DbConnection type out of test utilities; avoid redefining DB alias here.Production code should not import types from tests. Create apps/desktop/electron/database/types/db.ts and re-export shared types there; also avoid shadowing DB exported from ../db by renaming this local union (or import DB from ../db and introduce a new alias like DrizzleDb for any union use).
137-150: Wrap initialization in a single transaction.PRAGMA + seed insert + trigger creation should be atomic.
-async initializeDatabase(db: DbConnection | DB) { - await db.run(sql`PRAGMA user_version = 7`); - await db.insert(schema.field_properties).values({ - id: 1, - json_data: JSON.stringify(FieldPropertiesTemplates.HIGH_SCHOOL_FOOTBALL_FIELD_NO_END_ZONES), - }); - await createAllTriggers(db); -} +async initializeDatabase(db: DbConnection | DB) { + await (db as any).transaction?.(async (tx: any) => { + await tx.run(sql`PRAGMA user_version = 7`); + await tx.insert(schema.field_properties).values({ + id: 1, + json_data: JSON.stringify( + FieldPropertiesTemplates.HIGH_SCHOOL_FOOTBALL_FIELD_NO_END_ZONES, + ), + }); + await createAllTriggers(tx); + }); +}
62-71: Fix invalid runtime check:instanceof BetterSQLite3Databaseis a type, not a constructor.This will not work at runtime and can throw. Either:
- Restrict this service to BetterSQLite3 (preferred) and remove the union so you can call migrate(...) directly, or
- Add an explicit capability flag/constructor param and branch on that, not a TS type.
Example (restrict to BetterSQLite3):
- export type DB = BetterSQLite3Database<typeof schema> | DbConnection; + // Keep this service BetterSQLite3-only; run migrations in main process + type DrizzleDb = BetterSQLite3Database<typeof schema>; ... - if (this.db instanceof BetterSQLite3Database) { - await migrate(this.db, { migrationsFolder: folder, migrationsTable: "__drizzle_migrations" }); - } else { - throw new Error("Drizzle migrations are not supported for this database"); - } + await migrate(this.db as DrizzleDb, { + migrationsFolder: folder, + migrationsTable: "__drizzle_migrations", + });⛔ Skipped due to learnings
Learnt from: CR PR: OpenMarch/OpenMarch#0 File: CLAUDE.md:0-0 Timestamp: 2025-08-17T02:33:06.043Z Learning: Applies to apps/desktop/src/global/classes/*.ts : Use Drizzle-inferred types for DB entities (e.g., typeof table.$inferSelect) and avoid manually duplicating schema interfacesLearnt from: CR PR: OpenMarch/OpenMarch#0 File: .cursor/rules/database-interaction.mdc:0-0 Timestamp: 2025-08-17T02:33:33.561Z Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Ensure full TypeScript type safety, including exported Database[TableName], New[TableName]Args, and Modified[TableName]Args typesapps/desktop/electron/database/migrations/schema.ts (2)
184-185: Confirm migration for rotation_degrees exists and is NOT NULL DEFAULT 0.Schema adds rotation_degrees real().notNull().default(0) to marcher_pages. Ensure a migration (e.g., 0008) adds this column with the same constraints and backfills existing rows to 0.
14-22: Convert timestamps helper to a factory and update spreads.Reusing Drizzle builders across tables triggers “ColumnBuilder was already used.” Return fresh builders per call and spread ...timestamps().
-const timestamps = { - created_at: text().notNull().default(sql`(CURRENT_TIMESTAMP)`), - updated_at: text().notNull().default(sql`(CURRENT_TIMESTAMP)`).$onUpdate(() => sql`(CURRENT_TIMESTAMP)`), -}; +const timestamps = () => ({ + created_at: text().notNull().default(sql`(CURRENT_TIMESTAMP)`), + updated_at: text() + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`) + .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`), +}); @@ - ...timestamps, + ...timestamps(), @@ - ...timestamps, + ...timestamps(), @@ - ...timestamps, + ...timestamps(), @@ - ...timestamps, + ...timestamps(), @@ - ...timestamps, + ...timestamps(), @@ - ...timestamps, + ...timestamps(), @@ - ...timestamps, + ...timestamps(), @@ - ...timestamps, + ...timestamps(), @@ - ...timestamps, + ...timestamps(),Also applies to: 70-71, 103-104, 131-132, 140-141, 165-166, 209-210, 246-247, 252-253, 267-268, 284-285, 301-302
apps/desktop/electron/database/migrations/0006_bitter_proemial_gods.sql (1)
34-35: Fix DEFAULT timestamp expressions (literal string vs function).Use SQLite DEFAULT (CURRENT_TIMESTAMP), not a quoted literal.
- `created_at` text DEFAULT 'sql`(CURRENT_TIMESTAMP)`' NOT NULL, - `updated_at` text DEFAULT 'sql`(CURRENT_TIMESTAMP)`' NOT NULL, + `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + `updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,apps/desktop/electron/database/migrations/0007_misty_zemo.sql (1)
135-153: Make position_order NOT NULL and backfill before creating the unique index.Nullable allows multiple NULLs per shape_page_id, defeating uniqueness. Align with schema and backfill NULLs deterministically.
CREATE TABLE `__new_shape_page_marchers` ( `id` integer PRIMARY KEY NOT NULL, `shape_page_id` integer NOT NULL, `marcher_id` integer NOT NULL, - `position_order` integer, + `position_order` integer NOT NULL, `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, `updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, `notes` text, FOREIGN KEY (`shape_page_id`) REFERENCES `shape_pages`(`id`) ON UPDATE no action ON DELETE cascade, FOREIGN KEY (`marcher_id`) REFERENCES `marchers`(`id`) ON UPDATE no action ON DELETE cascade ); -- Backfill NULLs on insert to preserve uniqueness per shape_page_id -INSERT INTO `__new_shape_page_marchers`("id","shape_page_id","marcher_id","position_order","created_at","updated_at","notes") -SELECT "id","shape_page_id","marcher_id","position_order","created_at","updated_at","notes" FROM `shape_page_marchers`; +INSERT INTO `__new_shape_page_marchers`("id","shape_page_id","marcher_id","position_order","created_at","updated_at","notes") +SELECT sp.id, + sp.shape_page_id, + sp.marcher_id, + COALESCE( + sp.position_order, + (SELECT IFNULL(MAX(sp2.position_order), 0) + 1 + FROM shape_page_marchers sp2 + WHERE sp2.shape_page_id = sp.shape_page_id) + ) AS position_order, + sp.created_at, + sp.updated_at, + sp.notes + FROM shape_page_marchers sp;
| When running commands, start with the following "source ~/.zshrc" | ||
|
|
||
| When running vitest, use "npx vitest run {test_here}" |
There was a problem hiding this comment.
Clarify shell-agnostic setup instructions.
Requiring every contributor to run source ~/.zshrc before commands is brittle—bash (our default in several CI scripts) will warn or fail if that file doesn’t exist, and users on other shells (fish, PowerShell, bash on Windows) can’t follow it. Please make the setup instruction conditional (e.g., “if you’re in zsh and rely on aliases, run …”) or point to a more general environment bootstrap script instead.
🤖 Prompt for AI Agents
In .cursor/rules/command-syntax.mdc around lines 7–9, the instruction "source
~/.zshrc" is shell-specific and brittle; replace it with a brief, shell-agnostic
note that tells users to source their shell's startup file only if needed (e.g.,
"if you use zsh and rely on aliases, run source ~/.zshrc") or point to a common
project bootstrap script (e.g., "source ./scripts/bootstrap" or "source
~/.profile" for POSIX shells), and keep the vitest invocation as-is but ensure
the wording reflects it is universal (e.g., "Run tests: npx vitest run
{test_here}").
| "beats": { | ||
| "name": "beats", | ||
| "columns": { | ||
| "id": { | ||
| "name": "id", | ||
| "type": "integer", | ||
| "primaryKey": true, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "duration": { | ||
| "name": "duration", | ||
| "type": "real", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "position": { | ||
| "name": "position", | ||
| "type": "integer", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "include_in_measure": { | ||
| "name": "include_in_measure", | ||
| "type": "integer", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false, | ||
| "default": 1 | ||
| }, | ||
| "notes": { | ||
| "name": "notes", | ||
| "type": "text", | ||
| "primaryKey": false, | ||
| "notNull": false, | ||
| "autoincrement": false | ||
| }, | ||
| "created_at": { | ||
| "name": "created_at", | ||
| "type": "text", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false, | ||
| "default": "(CURRENT_TIMESTAMP)" | ||
| }, | ||
| "updated_at": { | ||
| "name": "updated_at", | ||
| "type": "text", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false, | ||
| "default": "(CURRENT_TIMESTAMP)" | ||
| } | ||
| }, | ||
| "indexes": {}, | ||
| "foreignKeys": {}, | ||
| "compositePrimaryKeys": {}, | ||
| "uniqueConstraints": {}, | ||
| "checkConstraints": { | ||
| "beats_duration_check": { | ||
| "name": "beats_duration_check", | ||
| "value": "duration >= 0" | ||
| }, | ||
| "beats_position_check": { | ||
| "name": "beats_position_check", | ||
| "value": "position >= 0" | ||
| }, | ||
| "beats_include_in_measure": { | ||
| "name": "beats_include_in_measure", | ||
| "value": "include_in_measure IN (0, 1)" | ||
| } | ||
| } | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
Enforce uniqueness for beats.position (schema/data integrity).
Code comments claim position is “Integer and unique,” but neither this snapshot nor schema adds a unique constraint. Add a unique index on beats(position) to prevent duplicate positions and keep page relations deterministic.
Would you like a follow-up migration (e.g., 0010) that adds:
CREATE UNIQUE INDEX IF NOT EXISTS beats_position_unique ON beats (position);🏁 Script executed:
#!/bin/bash
# List all migrations
ls apps/desktop/electron/database/migrations
# Find where the 'beats' table is created
rg -nP "createTable\(['\"]beats['\"]" -C3 apps/desktop/electron/database/migrations
# Search for any unique constraint or index on 'position'
rg -n "position" -C3 apps/desktop/electron/database/migrations | rg -C3 "unique"Length of output: 25931
Enforce uniqueness on beats.position
The JSDoc in apps/desktop/electron/database/migrations/schema.ts declares position as unique, but neither the snapshots nor migrations define this constraint.
- Add migration (e.g.,
0010_add_beats_position_unique.sql) with:CREATE UNIQUE INDEX IF NOT EXISTS beats_position_unique ON beats (position);
- Update the
positioncolumn definition inschema.tsto include.unique(), aligning implementation with the comment.
🤖 Prompt for AI Agents
In apps/desktop/electron/database/migrations/meta/0007_snapshot.json around
lines 69–143 the snapshot lacks the unique constraint for beats.position even
though schema.ts JSDoc declares it; add a new migration file (e.g.,
migrations/0010_add_beats_position_unique.sql) that runs a CREATE UNIQUE INDEX
IF NOT EXISTS beats_position_unique ON beats (position); and update
apps/desktop/electron/database/migrations/schema.ts to mark the position column
as unique (e.g., add .unique() to its column definition) so the schema and
migrations/snapshots stay consistent.
| import type { Database } from "sql.js"; | ||
| import { DefaultLogger } from "drizzle-orm/logger"; | ||
| import { | ||
| createTableRelationsHelpers, | ||
| extractTablesRelationalConfig, | ||
| type RelationalSchemaConfig, | ||
| type TablesRelationalConfig, | ||
| } from "drizzle-orm/relations"; | ||
| import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core/db"; | ||
| import { SQLiteSyncDialect } from "drizzle-orm/sqlite-core/dialect"; | ||
| import type { DrizzleConfig } from "drizzle-orm/utils"; | ||
| import { SQLJsSession } from "drizzle-orm/sql-js/session"; | ||
|
|
||
| export type SQLJsDatabase< | ||
| TSchema extends Record<string, unknown> = Record<string, never>, | ||
| > = BaseSQLiteDatabase<"sync", void, TSchema>; | ||
|
|
||
| export function drizzle< | ||
| TSchema extends Record<string, unknown> = Record<string, never>, | ||
| >( | ||
| client: Database, | ||
| config: DrizzleConfig<TSchema> = {}, | ||
| ): SQLJsDatabase<TSchema> { | ||
| const dialect = new SQLiteSyncDialect({ casing: config.casing }); | ||
| let logger; | ||
| if (config.logger === true) { | ||
| logger = new DefaultLogger(); | ||
| } else if (config.logger !== false) { | ||
| logger = config.logger; | ||
| } | ||
|
|
||
| let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined; | ||
| if (config.schema) { | ||
| const tablesConfig = extractTablesRelationalConfig( | ||
| config.schema, | ||
| createTableRelationsHelpers, | ||
| ); | ||
| schema = { | ||
| fullSchema: config.schema, | ||
| schema: tablesConfig.tables, | ||
| tableNamesMap: tablesConfig.tableNamesMap, | ||
| }; | ||
| } | ||
|
|
||
| const session = new SQLJsSession(client, dialect, schema, { logger }); | ||
| return new BaseSQLiteDatabase( | ||
| "sync", | ||
| dialect, | ||
| session, | ||
| schema, | ||
| ) as SQLJsDatabase<TSchema>; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Replace custom driver with Drizzle’s public sql-js adapter (removes brittle deep imports).
This file reimplements Drizzle’s sql-js adapter using internal modules (relations/sqlite-core/session/utils). That’s fragile across minor releases and unnecessary—public drizzle-orm/sql-js already supports schema, logger, and casing. Refactor to a thin wrapper to keep types stable and fix future upgrade risk.
Apply this diff to replace the file:
- import type { Database } from "sql.js";
- import { DefaultLogger } from "drizzle-orm/logger";
- import {
- createTableRelationsHelpers,
- extractTablesRelationalConfig,
- type RelationalSchemaConfig,
- type TablesRelationalConfig,
- } from "drizzle-orm/relations";
- import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core/db";
- import { SQLiteSyncDialect } from "drizzle-orm/sqlite-core/dialect";
- import type { DrizzleConfig } from "drizzle-orm/utils";
- import { SQLJsSession } from "drizzle-orm/sql-js/session";
-
- export type SQLJsDatabase<
- TSchema extends Record<string, unknown> = Record<string, never>,
- > = BaseSQLiteDatabase<"sync", void, TSchema>;
-
- export function drizzle<
- TSchema extends Record<string, unknown> = Record<string, never>,
- >(
- client: Database,
- config: DrizzleConfig<TSchema> = {},
- ): SQLJsDatabase<TSchema> {
- const dialect = new SQLiteSyncDialect({ casing: config.casing });
- let logger;
- if (config.logger === true) {
- logger = new DefaultLogger();
- } else if (config.logger !== false) {
- logger = config.logger;
- }
-
- let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
- if (config.schema) {
- const tablesConfig = extractTablesRelationalConfig(
- config.schema,
- createTableRelationsHelpers,
- );
- schema = {
- fullSchema: config.schema,
- schema: tablesConfig.tables,
- tableNamesMap: tablesConfig.tableNamesMap,
- };
- }
-
- const session = new SQLJsSession(client, dialect, schema, { logger });
- return new BaseSQLiteDatabase(
- "sync",
- dialect,
- session,
- schema,
- ) as SQLJsDatabase<TSchema>;
- }
+import type { Database } from "sql.js";
+import { drizzle as drizzleSqlJs, type SQLJsDatabase } from "drizzle-orm/sql-js";
+import type { DrizzleConfig } from "drizzle-orm";
+
+export type { SQLJsDatabase };
+
+export function drizzle<
+ TSchema extends Record<string, unknown> = Record<string, never>,
+>(
+ client: Database,
+ config: DrizzleConfig<TSchema> = {},
+): SQLJsDatabase<TSchema> {
+ return drizzleSqlJs<TSchema>(client, config);
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import type { Database } from "sql.js"; | |
| import { DefaultLogger } from "drizzle-orm/logger"; | |
| import { | |
| createTableRelationsHelpers, | |
| extractTablesRelationalConfig, | |
| type RelationalSchemaConfig, | |
| type TablesRelationalConfig, | |
| } from "drizzle-orm/relations"; | |
| import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core/db"; | |
| import { SQLiteSyncDialect } from "drizzle-orm/sqlite-core/dialect"; | |
| import type { DrizzleConfig } from "drizzle-orm/utils"; | |
| import { SQLJsSession } from "drizzle-orm/sql-js/session"; | |
| export type SQLJsDatabase< | |
| TSchema extends Record<string, unknown> = Record<string, never>, | |
| > = BaseSQLiteDatabase<"sync", void, TSchema>; | |
| export function drizzle< | |
| TSchema extends Record<string, unknown> = Record<string, never>, | |
| >( | |
| client: Database, | |
| config: DrizzleConfig<TSchema> = {}, | |
| ): SQLJsDatabase<TSchema> { | |
| const dialect = new SQLiteSyncDialect({ casing: config.casing }); | |
| let logger; | |
| if (config.logger === true) { | |
| logger = new DefaultLogger(); | |
| } else if (config.logger !== false) { | |
| logger = config.logger; | |
| } | |
| let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined; | |
| if (config.schema) { | |
| const tablesConfig = extractTablesRelationalConfig( | |
| config.schema, | |
| createTableRelationsHelpers, | |
| ); | |
| schema = { | |
| fullSchema: config.schema, | |
| schema: tablesConfig.tables, | |
| tableNamesMap: tablesConfig.tableNamesMap, | |
| }; | |
| } | |
| const session = new SQLJsSession(client, dialect, schema, { logger }); | |
| return new BaseSQLiteDatabase( | |
| "sync", | |
| dialect, | |
| session, | |
| schema, | |
| ) as SQLJsDatabase<TSchema>; | |
| } | |
| import type { Database } from "sql.js"; | |
| import { drizzle as drizzleSqlJs, type SQLJsDatabase } from "drizzle-orm/sql-js"; | |
| import type { DrizzleConfig } from "drizzle-orm"; | |
| export type { SQLJsDatabase }; | |
| export function drizzle< | |
| TSchema extends Record<string, unknown> = Record<string, never>, | |
| >( | |
| client: Database, | |
| config: DrizzleConfig<TSchema> = {}, | |
| ): SQLJsDatabase<TSchema> { | |
| return drizzleSqlJs<TSchema>(client, config); | |
| } |
| const session = new SQLJsSession(client, dialect, schema, { logger }); | ||
| return new BaseSQLiteDatabase( | ||
| "sync", | ||
| dialect, | ||
| session, | ||
| schema, | ||
| ) as SQLJsDatabase<TSchema>; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
Verify sql.js WASM asset handling in desktop packaging.
Ensure initSqlJs({ locateFile }) resolves sql-wasm.wasm at runtime in Electron. Missing asset wiring will fail DB init in renderer.
🏁 Script executed:
#!/bin/bash
# Check for sql.js initialization and wasm asset packaging
set -euo pipefail
echo "References to sql.js init and wasm:"
rg -n -g 'apps/desktop/**' -C2 -e 'initSqlJs\s*\(' -e 'locateFile' -e 'sql-wasm\.wasm' || true
echo
echo "Electron packaging config that might include wasm assets:"
fd -a package.json -HI apps/desktop | while read -r p; do
echo "--- $p"
jq -r '.build // empty' "$p" || true
doneLength of output: 1637
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Searching for initSqlJs usage in sqljs-driver.ts"
rg -n -C3 'initSqlJs' apps/desktop/electron/database/services/sqljs-driver.ts || true
echo
echo "Searching for class SQLJsSession definition"
rg -n -C3 'class SQLJsSession' apps/desktop/electron/database/services/sqljs-driver.ts || true
echo
echo "Searching for locateFile in sqljs-driver.ts"
rg -n -C3 'locateFile' apps/desktop/electron/database/services/sqljs-driver.ts || trueLength of output: 138
Include sql.js WASM in the Electron build and configure locateFile
In apps/desktop/electron/database/services/sqljs-driver.ts, initialize sql.js with a locateFile callback that points to the packaged WASM (e.g. from process.resourcesPath or your asar). Then in apps/desktop/package.json under build.files, add "node_modules/sql.js/dist/sql-wasm.wasm" (or the entire node_modules/sql.js/dist folder) so the WASM is bundled. Without this, the renderer’s DB init will fail.
🤖 Prompt for AI Agents
In apps/desktop/electron/database/services/sqljs-driver.ts around lines 45 to
51, the sql.js WASM is not being located during runtime causing DB init
failures; initialize sql.js before creating the SQLJsSession by calling its init
(or configure the initSqlJs) with a locateFile callback that resolves to the
packaged wasm (use process.resourcesPath or the asar path to point to
node_modules/sql.js/dist/sql-wasm.wasm), and update apps/desktop/package.json
build.files to include "node_modules/sql.js/dist/sql-wasm.wasm" (or the whole
"node_modules/sql.js/dist" folder) so the wasm is included in the Electron
build.
Almost a complete re-write of all database logic so that it is defined in the renderer using Drizzle ORM, rather than raw sql in the main process. Additionally, all the massive zustand stores that stored db data (e.g. marcherPages, beats, etc) are now using React Query
Database rewrite
The problem
The solution
transactionWithHistorywrapper that can be accessed in the renderer that allows developers to create custom batch functions without needing to write new db functionsBonus
better-sqlite3withsqljs, which drizzle supportsBetter testing
better-sqlite3andsqljsdescribeDbTestfunction andVITEST_ENABLE_BETTER_SQLITEenv variable will run the test suite with both database driversbetter-sqlite3is more performant, but we can't use it in a web versiontestWithHistoryfunction wraps the tests you use it with in a history checkexpectNumberOfChangesTanstack Query
Formerly React Query
The problem
marcherPageStoreortimingObjectsStorewould hold the data for the entire showThe soltuion
Bonuses
close #305
close #302
Summary by CodeRabbit
New Features
Changes
Localization
Documentation
Chores