stagent 0.6.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +21 -2
  2. package/dist/cli.js +226 -1
  3. package/docs/.coverage-gaps.json +66 -16
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/dashboard-kanban.md +13 -7
  6. package/docs/features/settings.md +15 -3
  7. package/docs/features/tables.md +122 -0
  8. package/docs/index.md +3 -2
  9. package/docs/journeys/developer.md +26 -16
  10. package/docs/journeys/personal-use.md +23 -9
  11. package/docs/journeys/power-user.md +40 -14
  12. package/docs/journeys/work-use.md +43 -15
  13. package/docs/manifest.json +27 -17
  14. package/package.json +3 -1
  15. package/src/app/api/chat/entities/search/route.ts +12 -3
  16. package/src/app/api/projects/[id]/route.ts +37 -0
  17. package/src/app/api/projects/__tests__/delete-project.test.ts +12 -0
  18. package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
  19. package/src/app/api/snapshots/[id]/route.ts +44 -0
  20. package/src/app/api/snapshots/route.ts +54 -0
  21. package/src/app/api/snapshots/settings/route.ts +67 -0
  22. package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
  23. package/src/app/api/tables/[id]/charts/route.ts +72 -0
  24. package/src/app/api/tables/[id]/columns/route.ts +70 -0
  25. package/src/app/api/tables/[id]/export/route.ts +94 -0
  26. package/src/app/api/tables/[id]/history/route.ts +15 -0
  27. package/src/app/api/tables/[id]/import/route.ts +111 -0
  28. package/src/app/api/tables/[id]/route.ts +86 -0
  29. package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
  30. package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
  31. package/src/app/api/tables/[id]/rows/route.ts +101 -0
  32. package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
  33. package/src/app/api/tables/[id]/triggers/route.ts +122 -0
  34. package/src/app/api/tables/route.ts +65 -0
  35. package/src/app/api/tables/templates/route.ts +92 -0
  36. package/src/app/globals.css +14 -0
  37. package/src/app/settings/page.tsx +2 -0
  38. package/src/app/tables/[id]/page.tsx +67 -0
  39. package/src/app/tables/page.tsx +21 -0
  40. package/src/app/tables/templates/page.tsx +19 -0
  41. package/src/components/book/book-reader.tsx +62 -9
  42. package/src/components/book/content-blocks.tsx +6 -1
  43. package/src/components/chat/chat-table-result.tsx +139 -0
  44. package/src/components/documents/document-browser.tsx +1 -1
  45. package/src/components/projects/project-form-sheet.tsx +3 -27
  46. package/src/components/schedules/schedule-form.tsx +5 -27
  47. package/src/components/settings/data-management-section.tsx +17 -12
  48. package/src/components/settings/database-snapshots-section.tsx +469 -0
  49. package/src/components/shared/app-sidebar.tsx +2 -0
  50. package/src/components/shared/document-picker-sheet.tsx +214 -11
  51. package/src/components/tables/table-browser.tsx +234 -0
  52. package/src/components/tables/table-cell-editor.tsx +226 -0
  53. package/src/components/tables/table-chart-builder.tsx +288 -0
  54. package/src/components/tables/table-chart-view.tsx +146 -0
  55. package/src/components/tables/table-column-header.tsx +103 -0
  56. package/src/components/tables/table-column-sheet.tsx +331 -0
  57. package/src/components/tables/table-create-sheet.tsx +240 -0
  58. package/src/components/tables/table-detail-sheet.tsx +144 -0
  59. package/src/components/tables/table-detail-tabs.tsx +278 -0
  60. package/src/components/tables/table-grid.tsx +61 -0
  61. package/src/components/tables/table-history-tab.tsx +148 -0
  62. package/src/components/tables/table-import-wizard.tsx +542 -0
  63. package/src/components/tables/table-list-table.tsx +95 -0
  64. package/src/components/tables/table-relation-combobox.tsx +217 -0
  65. package/src/components/tables/table-row-sheet.tsx +271 -0
  66. package/src/components/tables/table-spreadsheet.tsx +394 -0
  67. package/src/components/tables/table-template-gallery.tsx +162 -0
  68. package/src/components/tables/table-template-preview.tsx +219 -0
  69. package/src/components/tables/table-toolbar.tsx +79 -0
  70. package/src/components/tables/table-triggers-tab.tsx +446 -0
  71. package/src/components/tables/types.ts +6 -0
  72. package/src/components/tables/use-spreadsheet-keys.ts +171 -0
  73. package/src/components/tables/utils.ts +29 -0
  74. package/src/components/tasks/task-create-panel.tsx +5 -31
  75. package/src/components/tasks/task-edit-dialog.tsx +5 -27
  76. package/src/components/workflows/workflow-form-view.tsx +11 -35
  77. package/src/components/workflows/workflow-status-view.tsx +1 -1
  78. package/src/instrumentation.ts +3 -0
  79. package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
  80. package/src/lib/agents/claude-agent.ts +3 -1
  81. package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +23 -0
  82. package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +10 -0
  83. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +1 -1
  84. package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
  85. package/src/lib/agents/runtime/openai-direct.ts +29 -0
  86. package/src/lib/book/chapter-generator.ts +81 -5
  87. package/src/lib/book/chapter-mapping.ts +58 -24
  88. package/src/lib/book/content.ts +83 -47
  89. package/src/lib/book/markdown-parser.ts +1 -1
  90. package/src/lib/book/reading-paths.ts +8 -8
  91. package/src/lib/book/types.ts +1 -1
  92. package/src/lib/book/update-detector.ts +4 -1
  93. package/src/lib/chat/stagent-tools.ts +2 -0
  94. package/src/lib/chat/tool-catalog.ts +34 -0
  95. package/src/lib/chat/tools/table-tools.ts +955 -0
  96. package/src/lib/chat/tools/workflow-tools.ts +9 -1
  97. package/src/lib/constants/table-status.ts +68 -0
  98. package/src/lib/data/__tests__/clear.test.ts +1 -1
  99. package/src/lib/data/clear.ts +45 -0
  100. package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
  101. package/src/lib/data/seed-data/conversations.ts +350 -42
  102. package/src/lib/data/seed-data/documents.ts +564 -591
  103. package/src/lib/data/seed-data/learned-context.ts +101 -22
  104. package/src/lib/data/seed-data/notifications.ts +344 -70
  105. package/src/lib/data/seed-data/profile-test-results.ts +92 -11
  106. package/src/lib/data/seed-data/profiles.ts +144 -46
  107. package/src/lib/data/seed-data/projects.ts +50 -18
  108. package/src/lib/data/seed-data/repo-imports.ts +28 -13
  109. package/src/lib/data/seed-data/schedules.ts +208 -41
  110. package/src/lib/data/seed-data/table-templates.ts +234 -0
  111. package/src/lib/data/seed-data/tasks.ts +614 -116
  112. package/src/lib/data/seed-data/usage-ledger.ts +182 -103
  113. package/src/lib/data/seed-data/user-tables.ts +203 -0
  114. package/src/lib/data/seed-data/views.ts +52 -7
  115. package/src/lib/data/seed-data/workflows.ts +231 -84
  116. package/src/lib/data/seed.ts +55 -14
  117. package/src/lib/data/tables.ts +417 -0
  118. package/src/lib/db/bootstrap.ts +227 -0
  119. package/src/lib/db/index.ts +9 -0
  120. package/src/lib/db/migrations/0019_add_tables_feature.sql +160 -0
  121. package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
  122. package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
  123. package/src/lib/db/schema.ts +368 -0
  124. package/src/lib/snapshots/auto-backup.ts +132 -0
  125. package/src/lib/snapshots/retention.ts +64 -0
  126. package/src/lib/snapshots/snapshot-manager.ts +429 -0
  127. package/src/lib/tables/computed.ts +61 -0
  128. package/src/lib/tables/context-builder.ts +139 -0
  129. package/src/lib/tables/formula-engine.ts +415 -0
  130. package/src/lib/tables/history.ts +115 -0
  131. package/src/lib/tables/import.ts +343 -0
  132. package/src/lib/tables/query-builder.ts +152 -0
  133. package/src/lib/tables/trigger-evaluator.ts +146 -0
  134. package/src/lib/tables/types.ts +141 -0
  135. package/src/lib/tables/validation.ts +119 -0
  136. package/src/lib/utils/stagent-paths.ts +20 -0
  137. package/src/lib/workflows/types.ts +1 -1
  138. package/tsconfig.json +3 -1
  139. /package/docs/features/{playbook.md → user-guide.md} +0 -0
@@ -17,6 +17,18 @@ import {
17
17
  chatMessages,
18
18
  conversations,
19
19
  projectDocumentDefaults,
20
+ userTables,
21
+ userTableColumns,
22
+ userTableRows,
23
+ userTableViews,
24
+ userTableImports,
25
+ userTableRelationships,
26
+ tableDocumentInputs,
27
+ taskTableInputs,
28
+ workflowTableInputs,
29
+ scheduleTableInputs,
30
+ userTableTriggers,
31
+ userTableRowHistory,
20
32
  } from "@/lib/db/schema";
21
33
  import { eq, inArray } from "drizzle-orm";
22
34
  import { updateProjectSchema } from "@/lib/validators/project";
@@ -192,6 +204,31 @@ export async function DELETE(
192
204
  // 6. Project document defaults (junction table)
193
205
  db.delete(projectDocumentDefaults).where(eq(projectDocumentDefaults.projectId, id)).run();
194
206
 
207
+ // 6b. User-defined tables — cascade-delete children before parent
208
+ const tableIds = db
209
+ .select({ id: userTables.id })
210
+ .from(userTables)
211
+ .where(eq(userTables.projectId, id))
212
+ .all()
213
+ .map((r) => r.id);
214
+
215
+ if (tableIds.length > 0) {
216
+ // Junction tables first
217
+ db.delete(tableDocumentInputs).where(inArray(tableDocumentInputs.tableId, tableIds)).run();
218
+ db.delete(taskTableInputs).where(inArray(taskTableInputs.tableId, tableIds)).run();
219
+ db.delete(workflowTableInputs).where(inArray(workflowTableInputs.tableId, tableIds)).run();
220
+ db.delete(scheduleTableInputs).where(inArray(scheduleTableInputs.tableId, tableIds)).run();
221
+ // Children
222
+ db.delete(userTableRowHistory).where(inArray(userTableRowHistory.tableId, tableIds)).run();
223
+ db.delete(userTableTriggers).where(inArray(userTableTriggers.tableId, tableIds)).run();
224
+ db.delete(userTableImports).where(inArray(userTableImports.tableId, tableIds)).run();
225
+ db.delete(userTableViews).where(inArray(userTableViews.tableId, tableIds)).run();
226
+ db.delete(userTableRelationships).where(inArray(userTableRelationships.fromTableId, tableIds)).run();
227
+ db.delete(userTableRows).where(inArray(userTableRows.tableId, tableIds)).run();
228
+ db.delete(userTableColumns).where(inArray(userTableColumns.tableId, tableIds)).run();
229
+ db.delete(userTables).where(inArray(userTables.id, tableIds)).run();
230
+ }
231
+
195
232
  // 7. Direct project children
196
233
  db.delete(documents).where(eq(documents.projectId, id)).run();
197
234
  db.delete(tasks).where(eq(tasks.projectId, id)).run();
@@ -28,6 +28,7 @@ describe("project DELETE cascade coverage", () => {
28
28
  "environmentCheckpoints",
29
29
  "conversations",
30
30
  "projectDocumentDefaults",
31
+ "userTables",
31
32
  ];
32
33
 
33
34
  // Tables that are indirect children (FK to a table that has projectId)
@@ -39,6 +40,17 @@ describe("project DELETE cascade coverage", () => {
39
40
  { table: "chatMessages", parent: "conversations", via: "conversationId" },
40
41
  { table: "environmentArtifacts", parent: "environmentScans", via: "scanId" },
41
42
  { table: "environmentSyncOps", parent: "environmentCheckpoints", via: "checkpointId" },
43
+ { table: "userTableColumns", parent: "userTables", via: "tableId" },
44
+ { table: "userTableRows", parent: "userTables", via: "tableId" },
45
+ { table: "userTableViews", parent: "userTables", via: "tableId" },
46
+ { table: "userTableImports", parent: "userTables", via: "tableId" },
47
+ { table: "userTableRelationships", parent: "userTables", via: "fromTableId" },
48
+ { table: "tableDocumentInputs", parent: "userTables", via: "tableId" },
49
+ { table: "taskTableInputs", parent: "userTables", via: "tableId" },
50
+ { table: "workflowTableInputs", parent: "userTables", via: "tableId" },
51
+ { table: "scheduleTableInputs", parent: "userTables", via: "tableId" },
52
+ { table: "userTableTriggers", parent: "userTables", via: "tableId" },
53
+ { table: "userTableRowHistory", parent: "userTables", via: "tableId" },
42
54
  ];
43
55
 
44
56
  it("handles all tables with direct projectId FK", () => {
@@ -0,0 +1,62 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import {
3
+ restoreFromSnapshot,
4
+ isSnapshotLocked,
5
+ } from "@/lib/snapshots/snapshot-manager";
6
+ import { db } from "@/lib/db";
7
+ import { tasks } from "@/lib/db/schema";
8
+ import { eq } from "drizzle-orm";
9
+
10
+ /** POST /api/snapshots/[id]/restore — restore from a snapshot (destructive) */
11
+ export async function POST(
12
+ _req: NextRequest,
13
+ { params }: { params: Promise<{ id: string }> }
14
+ ) {
15
+ const { id } = await params;
16
+
17
+ if (isSnapshotLocked()) {
18
+ return NextResponse.json(
19
+ { error: "Another snapshot operation is already in progress" },
20
+ { status: 409 }
21
+ );
22
+ }
23
+
24
+ // Check for running tasks
25
+ const runningTasks = await db
26
+ .select()
27
+ .from(tasks)
28
+ .where(eq(tasks.status, "running"));
29
+
30
+ if (runningTasks.length > 0) {
31
+ return NextResponse.json(
32
+ {
33
+ error: `${runningTasks.length} task(s) are currently running. Stop them before restoring.`,
34
+ runningTasks: runningTasks.map((t) => ({
35
+ id: t.id,
36
+ title: t.title,
37
+ })),
38
+ },
39
+ { status: 409 }
40
+ );
41
+ }
42
+
43
+ try {
44
+ const result = await restoreFromSnapshot(id);
45
+
46
+ return NextResponse.json({
47
+ success: true,
48
+ requiresRestart: result.requiresRestart,
49
+ preRestoreSnapshotId: result.preRestoreSnapshotId,
50
+ message:
51
+ "Restore complete. Please restart the server to load the restored database.",
52
+ });
53
+ } catch (error) {
54
+ return NextResponse.json(
55
+ {
56
+ error:
57
+ error instanceof Error ? error.message : "Failed to restore snapshot",
58
+ },
59
+ { status: 500 }
60
+ );
61
+ }
62
+ }
@@ -0,0 +1,44 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getSnapshot, deleteSnapshot } from "@/lib/snapshots/snapshot-manager";
3
+
4
+ /** GET /api/snapshots/[id] — get snapshot details */
5
+ export async function GET(
6
+ _req: NextRequest,
7
+ { params }: { params: Promise<{ id: string }> }
8
+ ) {
9
+ const { id } = await params;
10
+
11
+ try {
12
+ const snapshot = await getSnapshot(id);
13
+ if (!snapshot) {
14
+ return NextResponse.json({ error: "Snapshot not found" }, { status: 404 });
15
+ }
16
+ return NextResponse.json(snapshot);
17
+ } catch (error) {
18
+ return NextResponse.json(
19
+ { error: error instanceof Error ? error.message : "Failed to get snapshot" },
20
+ { status: 500 }
21
+ );
22
+ }
23
+ }
24
+
25
+ /** DELETE /api/snapshots/[id] — delete a snapshot and its files */
26
+ export async function DELETE(
27
+ _req: NextRequest,
28
+ { params }: { params: Promise<{ id: string }> }
29
+ ) {
30
+ const { id } = await params;
31
+
32
+ try {
33
+ const deleted = await deleteSnapshot(id);
34
+ if (!deleted) {
35
+ return NextResponse.json({ error: "Snapshot not found" }, { status: 404 });
36
+ }
37
+ return NextResponse.json({ success: true });
38
+ } catch (error) {
39
+ return NextResponse.json(
40
+ { error: error instanceof Error ? error.message : "Failed to delete snapshot" },
41
+ { status: 500 }
42
+ );
43
+ }
44
+ }
@@ -0,0 +1,54 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import {
3
+ createSnapshot,
4
+ listSnapshots,
5
+ getSnapshotsSize,
6
+ isSnapshotLocked,
7
+ } from "@/lib/snapshots/snapshot-manager";
8
+
9
+ /** GET /api/snapshots — list all snapshots with disk usage */
10
+ export async function GET() {
11
+ try {
12
+ const [snapshotList, usage] = await Promise.all([
13
+ listSnapshots(),
14
+ getSnapshotsSize(),
15
+ ]);
16
+
17
+ return NextResponse.json({
18
+ snapshots: snapshotList,
19
+ totalBytes: usage.totalBytes,
20
+ snapshotCount: usage.snapshotCount,
21
+ });
22
+ } catch (error) {
23
+ return NextResponse.json(
24
+ { error: error instanceof Error ? error.message : "Failed to list snapshots" },
25
+ { status: 500 }
26
+ );
27
+ }
28
+ }
29
+
30
+ /** POST /api/snapshots — create a manual snapshot */
31
+ export async function POST(req: NextRequest) {
32
+ if (isSnapshotLocked()) {
33
+ return NextResponse.json(
34
+ { error: "Another snapshot operation is already in progress" },
35
+ { status: 409 }
36
+ );
37
+ }
38
+
39
+ try {
40
+ const body = await req.json().catch(() => ({}));
41
+ const label = typeof body.label === "string" && body.label.trim()
42
+ ? body.label.trim()
43
+ : `Manual snapshot`;
44
+
45
+ const snapshot = await createSnapshot(label, "manual");
46
+
47
+ return NextResponse.json(snapshot, { status: 201 });
48
+ } catch (error) {
49
+ return NextResponse.json(
50
+ { error: error instanceof Error ? error.message : "Failed to create snapshot" },
51
+ { status: 500 }
52
+ );
53
+ }
54
+ }
@@ -0,0 +1,67 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getSetting, setSetting } from "@/lib/settings/helpers";
3
+
4
+ const SETTINGS_KEYS = {
5
+ enabled: "snapshot.autoBackup.enabled",
6
+ interval: "snapshot.autoBackup.interval",
7
+ maxCount: "snapshot.retention.maxCount",
8
+ maxAgeWeeks: "snapshot.retention.maxAgeWeeks",
9
+ } as const;
10
+
11
+ const DEFAULTS = {
12
+ enabled: "false",
13
+ interval: "1d",
14
+ maxCount: "10",
15
+ maxAgeWeeks: "4",
16
+ } as const;
17
+
18
+ /** GET /api/snapshots/settings — read snapshot settings */
19
+ export async function GET() {
20
+ try {
21
+ const [enabled, interval, maxCount, maxAgeWeeks] = await Promise.all([
22
+ getSetting(SETTINGS_KEYS.enabled),
23
+ getSetting(SETTINGS_KEYS.interval),
24
+ getSetting(SETTINGS_KEYS.maxCount),
25
+ getSetting(SETTINGS_KEYS.maxAgeWeeks),
26
+ ]);
27
+
28
+ return NextResponse.json({
29
+ enabled: enabled ?? DEFAULTS.enabled,
30
+ interval: interval ?? DEFAULTS.interval,
31
+ maxCount: maxCount ?? DEFAULTS.maxCount,
32
+ maxAgeWeeks: maxAgeWeeks ?? DEFAULTS.maxAgeWeeks,
33
+ });
34
+ } catch (error) {
35
+ return NextResponse.json(
36
+ { error: error instanceof Error ? error.message : "Failed to read settings" },
37
+ { status: 500 }
38
+ );
39
+ }
40
+ }
41
+
42
+ /** PUT /api/snapshots/settings — update snapshot settings */
43
+ export async function PUT(req: NextRequest) {
44
+ try {
45
+ const body = await req.json();
46
+
47
+ if (body.enabled !== undefined) {
48
+ await setSetting(SETTINGS_KEYS.enabled, String(body.enabled));
49
+ }
50
+ if (body.interval !== undefined) {
51
+ await setSetting(SETTINGS_KEYS.interval, String(body.interval));
52
+ }
53
+ if (body.maxCount !== undefined) {
54
+ await setSetting(SETTINGS_KEYS.maxCount, String(body.maxCount));
55
+ }
56
+ if (body.maxAgeWeeks !== undefined) {
57
+ await setSetting(SETTINGS_KEYS.maxAgeWeeks, String(body.maxAgeWeeks));
58
+ }
59
+
60
+ return NextResponse.json({ success: true });
61
+ } catch (error) {
62
+ return NextResponse.json(
63
+ { error: error instanceof Error ? error.message : "Failed to save settings" },
64
+ { status: 500 }
65
+ );
66
+ }
67
+ }
@@ -0,0 +1,89 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import { userTableViews } from "@/lib/db/schema";
4
+ import { eq, and } from "drizzle-orm";
5
+
6
+ interface RouteContext {
7
+ params: Promise<{ id: string; chartId: string }>;
8
+ }
9
+
10
+ /** PATCH /api/tables/[id]/charts/[chartId] — Update a chart */
11
+ export async function PATCH(req: NextRequest, { params }: RouteContext) {
12
+ const { id, chartId } = await params;
13
+
14
+ const chart = db
15
+ .select()
16
+ .from(userTableViews)
17
+ .where(
18
+ and(
19
+ eq(userTableViews.id, chartId),
20
+ eq(userTableViews.tableId, id),
21
+ eq(userTableViews.type, "chart")
22
+ )
23
+ )
24
+ .get();
25
+
26
+ if (!chart) {
27
+ return NextResponse.json({ error: "Chart not found" }, { status: 404 });
28
+ }
29
+
30
+ const body = await req.json();
31
+ const existingConfig = chart.config ? JSON.parse(chart.config) : {};
32
+ const updates: Record<string, unknown> = { updatedAt: new Date() };
33
+
34
+ if (body.title !== undefined) updates.name = body.title;
35
+
36
+ // Merge config fields
37
+ const configUpdates: Record<string, unknown> = {};
38
+ if (body.type !== undefined) configUpdates.type = body.type;
39
+ if (body.xColumn !== undefined) configUpdates.xColumn = body.xColumn;
40
+ if (body.yColumn !== undefined) configUpdates.yColumn = body.yColumn;
41
+ if (body.aggregation !== undefined) configUpdates.aggregation = body.aggregation;
42
+
43
+ if (Object.keys(configUpdates).length > 0) {
44
+ updates.config = JSON.stringify({ ...existingConfig, ...configUpdates });
45
+ }
46
+
47
+ db.update(userTableViews)
48
+ .set(updates)
49
+ .where(eq(userTableViews.id, chartId))
50
+ .run();
51
+
52
+ const updated = db
53
+ .select()
54
+ .from(userTableViews)
55
+ .where(eq(userTableViews.id, chartId))
56
+ .get();
57
+
58
+ return NextResponse.json({
59
+ ...updated,
60
+ config: updated?.config ? JSON.parse(updated.config) : null,
61
+ });
62
+ }
63
+
64
+ /** DELETE /api/tables/[id]/charts/[chartId] — Remove a chart */
65
+ export async function DELETE(_req: NextRequest, { params }: RouteContext) {
66
+ const { id, chartId } = await params;
67
+
68
+ const chart = db
69
+ .select()
70
+ .from(userTableViews)
71
+ .where(
72
+ and(
73
+ eq(userTableViews.id, chartId),
74
+ eq(userTableViews.tableId, id),
75
+ eq(userTableViews.type, "chart")
76
+ )
77
+ )
78
+ .get();
79
+
80
+ if (!chart) {
81
+ return NextResponse.json({ error: "Chart not found" }, { status: 404 });
82
+ }
83
+
84
+ db.delete(userTableViews)
85
+ .where(eq(userTableViews.id, chartId))
86
+ .run();
87
+
88
+ return new NextResponse(null, { status: 204 });
89
+ }
@@ -0,0 +1,72 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { randomUUID } from "crypto";
3
+ import { db } from "@/lib/db";
4
+ import { userTableViews } from "@/lib/db/schema";
5
+ import { eq, and } from "drizzle-orm";
6
+ import { getTable } from "@/lib/data/tables";
7
+
8
+ interface RouteContext {
9
+ params: Promise<{ id: string }>;
10
+ }
11
+
12
+ /** GET /api/tables/[id]/charts — List chart views for a table */
13
+ export async function GET(_req: NextRequest, { params }: RouteContext) {
14
+ const { id } = await params;
15
+
16
+ const charts = db
17
+ .select()
18
+ .from(userTableViews)
19
+ .where(and(eq(userTableViews.tableId, id), eq(userTableViews.type, "chart")))
20
+ .all();
21
+
22
+ return NextResponse.json(
23
+ charts.map((c) => ({
24
+ ...c,
25
+ config: c.config ? JSON.parse(c.config) : null,
26
+ }))
27
+ );
28
+ }
29
+
30
+ /** POST /api/tables/[id]/charts — Save a new chart configuration */
31
+ export async function POST(req: NextRequest, { params }: RouteContext) {
32
+ const { id } = await params;
33
+
34
+ const table = await getTable(id);
35
+ if (!table) {
36
+ return NextResponse.json({ error: "Table not found" }, { status: 404 });
37
+ }
38
+
39
+ const body = await req.json();
40
+ const { type, title, xColumn, yColumn, aggregation } = body as {
41
+ type?: string;
42
+ title?: string;
43
+ xColumn?: string;
44
+ yColumn?: string;
45
+ aggregation?: string;
46
+ };
47
+
48
+ if (!type || !title || !xColumn) {
49
+ return NextResponse.json(
50
+ { error: "type, title, and xColumn are required" },
51
+ { status: 400 }
52
+ );
53
+ }
54
+
55
+ const viewId = randomUUID();
56
+ const now = new Date();
57
+
58
+ db.insert(userTableViews)
59
+ .values({
60
+ id: viewId,
61
+ tableId: id,
62
+ name: title,
63
+ type: "chart",
64
+ config: JSON.stringify({ type, xColumn, yColumn, aggregation }),
65
+ isDefault: false,
66
+ createdAt: now,
67
+ updatedAt: now,
68
+ })
69
+ .run();
70
+
71
+ return NextResponse.json({ id: viewId, name: title }, { status: 201 });
72
+ }
@@ -0,0 +1,70 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getTable, addColumn, reorderColumns } from "@/lib/data/tables";
3
+ import {
4
+ addColumnSchema,
5
+ reorderColumnsSchema,
6
+ } from "@/lib/tables/validation";
7
+
8
+ export async function POST(
9
+ req: NextRequest,
10
+ { params }: { params: Promise<{ id: string }> }
11
+ ) {
12
+ const { id } = await params;
13
+
14
+ try {
15
+ const existing = await getTable(id);
16
+ if (!existing) {
17
+ return NextResponse.json({ error: "Table not found" }, { status: 404 });
18
+ }
19
+
20
+ const body = await req.json();
21
+ const parsed = addColumnSchema.safeParse(body);
22
+ if (!parsed.success) {
23
+ return NextResponse.json(
24
+ { error: parsed.error.flatten() },
25
+ { status: 400 }
26
+ );
27
+ }
28
+
29
+ const column = await addColumn(id, parsed.data);
30
+ return NextResponse.json(column, { status: 201 });
31
+ } catch (err) {
32
+ console.error("[tables] POST column error:", err);
33
+ return NextResponse.json(
34
+ { error: "Failed to add column" },
35
+ { status: 500 }
36
+ );
37
+ }
38
+ }
39
+
40
+ export async function PATCH(
41
+ req: NextRequest,
42
+ { params }: { params: Promise<{ id: string }> }
43
+ ) {
44
+ const { id } = await params;
45
+
46
+ try {
47
+ const existing = await getTable(id);
48
+ if (!existing) {
49
+ return NextResponse.json({ error: "Table not found" }, { status: 404 });
50
+ }
51
+
52
+ const body = await req.json();
53
+ const parsed = reorderColumnsSchema.safeParse(body);
54
+ if (!parsed.success) {
55
+ return NextResponse.json(
56
+ { error: parsed.error.flatten() },
57
+ { status: 400 }
58
+ );
59
+ }
60
+
61
+ const columns = await reorderColumns(id, parsed.data.columnIds);
62
+ return NextResponse.json(columns);
63
+ } catch (err) {
64
+ console.error("[tables] PATCH columns reorder error:", err);
65
+ return NextResponse.json(
66
+ { error: "Failed to reorder columns" },
67
+ { status: 500 }
68
+ );
69
+ }
70
+ }
@@ -0,0 +1,94 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getTable, listRows } from "@/lib/data/tables";
3
+ import type { ColumnDef } from "@/lib/tables/types";
4
+
5
+ export async function GET(
6
+ req: NextRequest,
7
+ { params }: { params: Promise<{ id: string }> }
8
+ ) {
9
+ const { id } = await params;
10
+
11
+ try {
12
+ const table = await getTable(id);
13
+ if (!table) {
14
+ return NextResponse.json({ error: "Table not found" }, { status: 404 });
15
+ }
16
+
17
+ const url = new URL(req.url);
18
+ const format = url.searchParams.get("format") ?? "csv";
19
+
20
+ let columns: ColumnDef[] = [];
21
+ try {
22
+ columns = JSON.parse(table.columnSchema) as ColumnDef[];
23
+ } catch {
24
+ columns = [];
25
+ }
26
+
27
+ // Fetch all rows (up to 10000)
28
+ const rows = await listRows(id, { limit: 10000 });
29
+ const parsedRows = rows.map((r) => JSON.parse(r.data) as Record<string, unknown>);
30
+
31
+ switch (format) {
32
+ case "json":
33
+ return new NextResponse(JSON.stringify(parsedRows, null, 2), {
34
+ headers: {
35
+ "Content-Type": "application/json",
36
+ "Content-Disposition": `attachment; filename="${table.name}.json"`,
37
+ },
38
+ });
39
+
40
+ case "xlsx": {
41
+ const ExcelJS = await import("exceljs");
42
+ const workbook = new ExcelJS.Workbook();
43
+ const worksheet = workbook.addWorksheet(table.name);
44
+
45
+ // Add header row
46
+ worksheet.addRow(columns.map((c) => c.displayName));
47
+
48
+ // Add data rows
49
+ for (const row of parsedRows) {
50
+ worksheet.addRow(columns.map((c) => row[c.name] ?? ""));
51
+ }
52
+
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ const buffer = await workbook.xlsx.writeBuffer() as any;
55
+ return new NextResponse(buffer, {
56
+ headers: {
57
+ "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
58
+ "Content-Disposition": `attachment; filename="${table.name}.xlsx"`,
59
+ },
60
+ });
61
+ }
62
+
63
+ case "csv":
64
+ default: {
65
+ const lines: string[] = [];
66
+ // Header
67
+ lines.push(columns.map((c) => escapeCsvField(c.displayName)).join(","));
68
+ // Data rows
69
+ for (const row of parsedRows) {
70
+ lines.push(
71
+ columns.map((c) => escapeCsvField(String(row[c.name] ?? ""))).join(",")
72
+ );
73
+ }
74
+ const csv = lines.join("\n");
75
+ return new NextResponse(csv, {
76
+ headers: {
77
+ "Content-Type": "text/csv; charset=utf-8",
78
+ "Content-Disposition": `attachment; filename="${table.name}.csv"`,
79
+ },
80
+ });
81
+ }
82
+ }
83
+ } catch (err) {
84
+ console.error("[tables/export] GET error:", err);
85
+ return NextResponse.json({ error: "Failed to export table" }, { status: 500 });
86
+ }
87
+ }
88
+
89
+ function escapeCsvField(value: string): string {
90
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
91
+ return `"${value.replace(/"/g, '""')}"`;
92
+ }
93
+ return value;
94
+ }
@@ -0,0 +1,15 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getTableHistory } from "@/lib/tables/history";
3
+
4
+ interface RouteContext {
5
+ params: Promise<{ id: string }>;
6
+ }
7
+
8
+ export async function GET(req: NextRequest, { params }: RouteContext) {
9
+ const { id } = await params;
10
+ const url = new URL(req.url);
11
+ const limit = Math.min(parseInt(url.searchParams.get("limit") ?? "100", 10), 500);
12
+
13
+ const history = getTableHistory(id, limit);
14
+ return NextResponse.json(history);
15
+ }