mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 21:26:33 +00:00
fix: drop legacy layouts.template_id/display_id columns via table rebuild
- Migration v0.5: rebuild layouts table without template_id, display_id, regions, grid_cols, grid_rows, is_default - Migration v0.6: rebuild layout_cells table without region_name - Migration v0.7: drop layout_templates table entirely (concept removed) - createLayout simplified to clean column set (no sentinel values) - createLayoutCell simplified (no region_name placeholder) - Removed all layout_template repo methods (dead code) Answers user question "why template_id/display_id in template": SQLite can't drop columns without rebuilding the table. Now done properly.
This commit is contained in:
parent
b8f934b2be
commit
2398be6853
2 changed files with 73 additions and 82 deletions
|
|
@ -366,4 +366,70 @@ export const MIGRATIONS: readonly MigrationEntry[] = [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ---- v0.5: rebuild layouts table to drop legacy columns
|
||||||
|
// SQLite can't drop columns, so rebuild: create new schema → copy data →
|
||||||
|
// drop old → rename. Removes template_id, display_id, regions, grid_cols,
|
||||||
|
// grid_rows, is_default — cells own position now, displays attach via join.
|
||||||
|
(db: DatabaseSync) => {
|
||||||
|
const cols = db.prepare(`PRAGMA table_info("layouts")`).all() as Array<{ name: string }>;
|
||||||
|
const hasTemplateId = cols.some((c) => c.name === "template_id");
|
||||||
|
if (!hasTemplateId) return; // already migrated
|
||||||
|
|
||||||
|
db.exec("PRAGMA foreign_keys = OFF");
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE layouts_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
description TEXT,
|
||||||
|
priority TEXT NOT NULL DEFAULT 'normal' CHECK(priority IN ('hot', 'normal', 'cold')),
|
||||||
|
cooling_timeout_seconds INTEGER,
|
||||||
|
preload_camera_ids TEXT NOT NULL DEFAULT '[]',
|
||||||
|
resets_idle_timer INTEGER NOT NULL DEFAULT 1
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
INSERT INTO layouts_new (id, name, description, priority, cooling_timeout_seconds, preload_camera_ids, resets_idle_timer)
|
||||||
|
SELECT id, name, description, priority, cooling_timeout_seconds, preload_camera_ids, resets_idle_timer FROM layouts;
|
||||||
|
|
||||||
|
DROP TABLE layouts;
|
||||||
|
ALTER TABLE layouts_new RENAME TO layouts;
|
||||||
|
`);
|
||||||
|
db.exec("PRAGMA foreign_keys = ON");
|
||||||
|
},
|
||||||
|
|
||||||
|
// Same cleanup for layout_cells — drop region_name, layout_id FK stays
|
||||||
|
(db: DatabaseSync) => {
|
||||||
|
const cols = db.prepare(`PRAGMA table_info("layout_cells")`).all() as Array<{ name: string }>;
|
||||||
|
const hasRegionName = cols.some((c) => c.name === "region_name");
|
||||||
|
if (!hasRegionName) return;
|
||||||
|
|
||||||
|
db.exec("PRAGMA foreign_keys = OFF");
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE layout_cells_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
layout_id INTEGER NOT NULL REFERENCES layouts(id) ON DELETE CASCADE,
|
||||||
|
row INTEGER NOT NULL DEFAULT 0,
|
||||||
|
col INTEGER NOT NULL DEFAULT 0,
|
||||||
|
row_span INTEGER NOT NULL DEFAULT 1,
|
||||||
|
col_span INTEGER NOT NULL DEFAULT 1,
|
||||||
|
content_type TEXT NOT NULL CHECK(content_type IN ('camera', 'web', 'html')),
|
||||||
|
camera_id INTEGER REFERENCES cameras(id) ON DELETE SET NULL,
|
||||||
|
stream_selector TEXT,
|
||||||
|
web_url TEXT,
|
||||||
|
html_content TEXT,
|
||||||
|
cooling_timeout_seconds INTEGER,
|
||||||
|
options TEXT NOT NULL DEFAULT '{}'
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
INSERT INTO layout_cells_new (id, layout_id, row, col, row_span, col_span, content_type, camera_id, stream_selector, web_url, html_content, cooling_timeout_seconds, options)
|
||||||
|
SELECT id, layout_id, row, col, row_span, col_span, content_type, camera_id, stream_selector, web_url, html_content, cooling_timeout_seconds, options FROM layout_cells;
|
||||||
|
|
||||||
|
DROP TABLE layout_cells;
|
||||||
|
ALTER TABLE layout_cells_new RENAME TO layout_cells;
|
||||||
|
`);
|
||||||
|
db.exec("PRAGMA foreign_keys = ON");
|
||||||
|
},
|
||||||
|
|
||||||
|
// Drop layout_templates entirely — concept removed
|
||||||
|
`DROP TABLE IF EXISTS layout_templates`,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -415,61 +415,6 @@ export class Repository {
|
||||||
// layout templates
|
// layout templates
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
listLayoutTemplates(): LayoutTemplate[] {
|
|
||||||
const rs = this.prep("SELECT * FROM layout_templates ORDER BY name").all();
|
|
||||||
return rs.map((r) => rowToLayoutTemplate(r as Record<string, unknown>));
|
|
||||||
}
|
|
||||||
|
|
||||||
getLayoutTemplateById(id: number): LayoutTemplate | null {
|
|
||||||
const r = this.prep("SELECT * FROM layout_templates WHERE id = ?").get(id);
|
|
||||||
return r ? rowToLayoutTemplate(r as Record<string, unknown>) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
createLayoutTemplate(input: {
|
|
||||||
name: string;
|
|
||||||
description?: string | null;
|
|
||||||
regions: unknown;
|
|
||||||
grid_cols?: number;
|
|
||||||
grid_rows?: number;
|
|
||||||
is_builtin?: boolean;
|
|
||||||
}): LayoutTemplate {
|
|
||||||
const result = this.prep(
|
|
||||||
`INSERT INTO layout_templates (name, description, regions, grid_cols, grid_rows, is_builtin)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
||||||
).run(
|
|
||||||
input.name,
|
|
||||||
input.description ?? null,
|
|
||||||
J(input.regions),
|
|
||||||
input.grid_cols ?? 12,
|
|
||||||
input.grid_rows ?? 12,
|
|
||||||
B(input.is_builtin ?? false),
|
|
||||||
);
|
|
||||||
const id = Number(result.lastInsertRowid);
|
|
||||||
void this.notify("layout_templates", "create", id);
|
|
||||||
const r = this.getLayoutTemplateById(id);
|
|
||||||
if (!r) throw new Error("layout_template vanished after insert");
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateLayoutTemplate(id: number, patch: { name?: string; description?: string | null; regions?: unknown; grid_cols?: number; grid_rows?: number }): void {
|
|
||||||
const sets: string[] = [];
|
|
||||||
const vals: unknown[] = [];
|
|
||||||
for (const [k, v] of Object.entries(patch)) {
|
|
||||||
if (k === "id") continue;
|
|
||||||
sets.push(`${k} = ?`);
|
|
||||||
vals.push(k === "regions" ? J(v) : (v === undefined ? null : v));
|
|
||||||
}
|
|
||||||
if (sets.length === 0) return;
|
|
||||||
vals.push(id);
|
|
||||||
this.db.prepare(`UPDATE layout_templates SET ${sets.join(", ")} WHERE id = ?`).run(...vals as any[]);
|
|
||||||
void this.notify("layout_templates", "update", id);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteLayoutTemplate(id: number): void {
|
|
||||||
this.db.prepare(`DELETE FROM layout_templates WHERE id = ?`).run(id);
|
|
||||||
void this.notify("layout_templates", "delete", id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// layouts
|
// layouts
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
@ -546,25 +491,15 @@ export class Repository {
|
||||||
preload_camera_ids?: number[];
|
preload_camera_ids?: number[];
|
||||||
resets_idle_timer?: boolean;
|
resets_idle_timer?: boolean;
|
||||||
}): Layout {
|
}): Layout {
|
||||||
// Legacy NOT NULL columns (template_id, display_id, regions, grid_*) are
|
|
||||||
// populated with sentinel values: cells own their position now and the
|
|
||||||
// grid is computed at read time. The columns will be dropped by a future
|
|
||||||
// migration — until then they're inert.
|
|
||||||
const result = this.prep(
|
const result = this.prep(
|
||||||
`INSERT INTO layouts (name, description, template_id, regions, grid_cols, grid_rows, display_id, priority, cooling_timeout_seconds, preload_camera_ids, is_default, resets_idle_timer)
|
`INSERT INTO layouts (name, description, priority, cooling_timeout_seconds, preload_camera_ids, resets_idle_timer)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
).run(
|
).run(
|
||||||
input.name,
|
input.name,
|
||||||
input.description ?? null,
|
input.description ?? null,
|
||||||
0,
|
|
||||||
J([]),
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
input.priority ?? "normal",
|
input.priority ?? "normal",
|
||||||
input.cooling_timeout_seconds ?? null,
|
input.cooling_timeout_seconds ?? null,
|
||||||
J(input.preload_camera_ids ?? []),
|
J(input.preload_camera_ids ?? []),
|
||||||
B(false),
|
|
||||||
B(input.resets_idle_timer ?? true),
|
B(input.resets_idle_timer ?? true),
|
||||||
);
|
);
|
||||||
const id = Number(result.lastInsertRowid);
|
const id = Number(result.lastInsertRowid);
|
||||||
|
|
@ -618,15 +553,11 @@ export class Repository {
|
||||||
cooling_timeout_seconds?: number | null;
|
cooling_timeout_seconds?: number | null;
|
||||||
options?: Record<string, unknown>;
|
options?: Record<string, unknown>;
|
||||||
}): LayoutCell {
|
}): LayoutCell {
|
||||||
// region_name column is legacy NOT NULL — synthesize a unique placeholder
|
|
||||||
// until the column is dropped by a future migration. Nothing reads it.
|
|
||||||
const placeholder = `cell_${input.layout_id}_${input.row}_${input.col}_${Date.now()}`;
|
|
||||||
const result = this.prep(
|
const result = this.prep(
|
||||||
`INSERT INTO layout_cells (layout_id, region_name, "row", col, row_span, col_span, content_type, camera_id, stream_selector, web_url, html_content, cooling_timeout_seconds, options)
|
`INSERT INTO layout_cells (layout_id, "row", col, row_span, col_span, content_type, camera_id, stream_selector, web_url, html_content, cooling_timeout_seconds, options)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
).run(
|
).run(
|
||||||
input.layout_id,
|
input.layout_id,
|
||||||
placeholder,
|
|
||||||
input.row,
|
input.row,
|
||||||
input.col,
|
input.col,
|
||||||
input.row_span ?? 1,
|
input.row_span ?? 1,
|
||||||
|
|
@ -1156,15 +1087,9 @@ export class Repository {
|
||||||
return this.listLayoutCells(layoutId);
|
return this.listLayoutCells(layoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
layoutTemplates(ids: number[]): LayoutTemplate[] {
|
// Deprecated — layout_templates dropped in v0.5
|
||||||
if (ids.length === 0) return [];
|
layoutTemplates(_ids: number[]): LayoutTemplate[] {
|
||||||
const placeholders = ids.map(() => "?").join(",");
|
return [];
|
||||||
const rs = this.db
|
|
||||||
.prepare(
|
|
||||||
`SELECT * FROM layout_templates WHERE id IN (${placeholders})`,
|
|
||||||
)
|
|
||||||
.all(...(ids as never[]));
|
|
||||||
return rs.map((r) => rowToLayoutTemplate(r as Record<string, unknown>));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cameraLabelNames(cameraId: number): string[] {
|
cameraLabelNames(cameraId: number): string[] {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue