feat(FN-3162): add plugin-owned roadmap schema hook and store bootstrap
The merge introduces a plugin-owned roadmap schema system (FN-3162), allowing plugins to define their own schema initialization hook, with tests bootstrapped in the roadmap store and documentation added to the plugin authoring guide. The parallel FN-3281 work delivers review revisions, updates the l Fusion-Task-Id: FN-3162
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
|
||||
- `manifest.json` — plugin metadata and dashboard view declaration
|
||||
- `src/index.ts` — plugin definition (`onSchemaInit`, routes, dashboard view metadata)
|
||||
- `src/roadmap-schema.ts` — canonical roadmap DDL used by `hooks.onSchemaInit`
|
||||
- `src/server/index.ts` — backend server exports
|
||||
- `src/dashboard-view.tsx` — dashboard view entry export for host registration
|
||||
- `src/dashboard/RoadmapsView.tsx` — plugin-owned roadmap planner page
|
||||
@@ -28,4 +29,6 @@
|
||||
|
||||
## Notes
|
||||
|
||||
Roadmap tables are plugin-owned and created via `hooks.onSchemaInit` in `src/index.ts`, which delegates to `src/roadmap-schema.ts`. Core database bootstrap no longer creates roadmap tables/indexes.
|
||||
|
||||
The plugin keeps a single canonical dashboard entrypoint (`./dashboard-view`) and accepts host-supplied dashboard context (`projectId`, optional `addToast`). Do not deep-import dashboard internals from this plugin.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { mkdtempSync, readFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { Database } from "@fusion/core";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import plugin, {
|
||||
RoadmapStore,
|
||||
applyRoadmapFeatureReorder,
|
||||
@@ -15,6 +18,13 @@ import plugin, {
|
||||
} from "../index.js";
|
||||
|
||||
describe("roadmap-planner package surface", () => {
|
||||
const tmpDirs: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tmpDirs.map((dir) => rm(dir, { recursive: true, force: true })));
|
||||
tmpDirs.length = 0;
|
||||
});
|
||||
|
||||
it("keeps manifest and plugin entry metadata aligned", () => {
|
||||
const manifest = JSON.parse(readFileSync(resolve(process.cwd(), "manifest.json"), "utf8")) as {
|
||||
id: string;
|
||||
@@ -41,6 +51,32 @@ describe("roadmap-planner package surface", () => {
|
||||
expect(plugin.manifest.id).toBe("roadmap-planner");
|
||||
});
|
||||
|
||||
it("registers onSchemaInit hook that creates roadmap tables and indexes", () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "roadmap-plugin-schema-test-"));
|
||||
tmpDirs.push(tmpDir);
|
||||
|
||||
const db = new Database(join(tmpDir, ".fusion"), { inMemory: true });
|
||||
db.init();
|
||||
|
||||
expect(plugin.hooks?.onSchemaInit).toBeTypeOf("function");
|
||||
plugin.hooks?.onSchemaInit?.(db);
|
||||
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all() as Array<{ name: string }>;
|
||||
const indexes = db.prepare("SELECT name FROM sqlite_master WHERE type = 'index'").all() as Array<{ name: string }>;
|
||||
|
||||
expect(tables.map((row) => row.name)).toEqual(expect.arrayContaining([
|
||||
"roadmaps",
|
||||
"roadmap_milestones",
|
||||
"roadmap_features",
|
||||
]));
|
||||
expect(indexes.map((row) => row.name)).toEqual(expect.arrayContaining([
|
||||
"idxRoadmapMilestonesRoadmapOrder",
|
||||
"idxRoadmapFeaturesMilestoneOrder",
|
||||
]));
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("re-exports roadmap domain symbols", () => {
|
||||
expect(typeof normalizeRoadmapMilestoneOrder).toBe("function");
|
||||
expect(typeof applyRoadmapMilestoneReorder).toBe("function");
|
||||
|
||||
@@ -1,46 +1,6 @@
|
||||
import type { Database } from "@fusion/core";
|
||||
import { definePlugin } from "@fusion/plugin-sdk";
|
||||
import { createRoadmapPluginRoutes } from "./routes/roadmap-routes.js";
|
||||
|
||||
export function ensureRoadmapSchema(db: Database): void {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS roadmaps (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS roadmap_milestones (
|
||||
id TEXT PRIMARY KEY,
|
||||
roadmapId TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
orderIndex INTEGER NOT NULL,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL,
|
||||
FOREIGN KEY (roadmapId) REFERENCES roadmaps(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS roadmap_features (
|
||||
id TEXT PRIMARY KEY,
|
||||
milestoneId TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
orderIndex INTEGER NOT NULL,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL,
|
||||
FOREIGN KEY (milestoneId) REFERENCES roadmap_milestones(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idxRoadmapMilestonesRoadmapOrder
|
||||
ON roadmap_milestones(roadmapId, orderIndex, createdAt, id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idxRoadmapFeaturesMilestoneOrder
|
||||
ON roadmap_features(milestoneId, orderIndex, createdAt, id);
|
||||
`);
|
||||
}
|
||||
import { ensureRoadmapSchema } from "./roadmap-schema.js";
|
||||
|
||||
const plugin = definePlugin({
|
||||
manifest: {
|
||||
@@ -109,5 +69,6 @@ export {
|
||||
export { RoadmapStore } from "./store/roadmap-store.js";
|
||||
export type { RoadmapStoreEvents } from "./store/roadmap-store.js";
|
||||
|
||||
export { ensureRoadmapSchema } from "./roadmap-schema.js";
|
||||
export { RoadmapDashboardView } from "./dashboard-view.js";
|
||||
export * from "./server/index.js";
|
||||
|
||||
41
plugins/fusion-plugin-roadmap/src/roadmap-schema.ts
Normal file
41
plugins/fusion-plugin-roadmap/src/roadmap-schema.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Database } from "@fusion/core";
|
||||
|
||||
export function ensureRoadmapSchema(db: Database): void {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS roadmaps (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS roadmap_milestones (
|
||||
id TEXT PRIMARY KEY,
|
||||
roadmapId TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
orderIndex INTEGER NOT NULL,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL,
|
||||
FOREIGN KEY (roadmapId) REFERENCES roadmaps(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS roadmap_features (
|
||||
id TEXT PRIMARY KEY,
|
||||
milestoneId TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
orderIndex INTEGER NOT NULL,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL,
|
||||
FOREIGN KEY (milestoneId) REFERENCES roadmap_milestones(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idxRoadmapMilestonesRoadmapOrder
|
||||
ON roadmap_milestones(roadmapId, orderIndex, createdAt, id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idxRoadmapFeaturesMilestoneOrder
|
||||
ON roadmap_features(milestoneId, orderIndex, createdAt, id);
|
||||
`);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { Database, createDatabase } from "@fusion/core";
|
||||
import { RoadmapStore } from "../roadmap-store.js";
|
||||
import { ensureRoadmapSchema } from "../../roadmap-schema.js";
|
||||
import type {
|
||||
RoadmapCreateInput,
|
||||
RoadmapUpdateInput,
|
||||
@@ -33,6 +34,7 @@ describe("RoadmapStore", () => {
|
||||
// Database instances explicitly (search for `persistDb`).
|
||||
db = new Database(join(tmpDir, ".fusion"), { inMemory: true });
|
||||
db.init();
|
||||
ensureRoadmapSchema(db);
|
||||
store = new RoadmapStore(db);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user