stagent 0.6.2 → 0.7.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.
- package/README.md +21 -2
- package/dist/cli.js +272 -1
- package/docs/.coverage-gaps.json +66 -16
- package/docs/.last-generated +1 -1
- package/docs/features/dashboard-kanban.md +13 -7
- package/docs/features/settings.md +15 -3
- package/docs/features/tables.md +122 -0
- package/docs/index.md +3 -2
- package/docs/journeys/developer.md +26 -16
- package/docs/journeys/personal-use.md +23 -9
- package/docs/journeys/power-user.md +40 -14
- package/docs/journeys/work-use.md +43 -15
- package/docs/manifest.json +27 -17
- package/package.json +3 -2
- package/src/app/api/chat/entities/search/route.ts +12 -3
- package/src/app/api/documents/[id]/route.ts +5 -1
- package/src/app/api/documents/[id]/versions/route.ts +53 -0
- package/src/app/api/documents/route.ts +5 -1
- package/src/app/api/projects/[id]/documents/route.ts +124 -0
- package/src/app/api/projects/[id]/route.ts +72 -3
- package/src/app/api/projects/__tests__/delete-project.test.ts +13 -0
- package/src/app/api/schedules/route.ts +19 -1
- package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
- package/src/app/api/snapshots/[id]/route.ts +44 -0
- package/src/app/api/snapshots/route.ts +54 -0
- package/src/app/api/snapshots/settings/route.ts +67 -0
- package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
- package/src/app/api/tables/[id]/charts/route.ts +72 -0
- package/src/app/api/tables/[id]/columns/route.ts +70 -0
- package/src/app/api/tables/[id]/export/route.ts +94 -0
- package/src/app/api/tables/[id]/history/route.ts +15 -0
- package/src/app/api/tables/[id]/import/route.ts +111 -0
- package/src/app/api/tables/[id]/route.ts +86 -0
- package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
- package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
- package/src/app/api/tables/[id]/rows/route.ts +101 -0
- package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
- package/src/app/api/tables/[id]/triggers/route.ts +122 -0
- package/src/app/api/tables/route.ts +65 -0
- package/src/app/api/tables/templates/route.ts +92 -0
- package/src/app/api/tasks/[id]/route.ts +37 -2
- package/src/app/api/tasks/[id]/siblings/route.ts +48 -0
- package/src/app/api/tasks/route.ts +8 -9
- package/src/app/api/workflows/[id]/documents/route.ts +209 -0
- package/src/app/api/workflows/[id]/execute/route.ts +6 -2
- package/src/app/api/workflows/[id]/route.ts +16 -3
- package/src/app/api/workflows/[id]/status/route.ts +18 -2
- package/src/app/api/workflows/route.ts +13 -2
- package/src/app/documents/page.tsx +5 -1
- package/src/app/layout.tsx +0 -1
- package/src/app/manifest.ts +3 -3
- package/src/app/projects/[id]/page.tsx +62 -2
- package/src/app/settings/page.tsx +2 -0
- package/src/app/tables/[id]/page.tsx +67 -0
- package/src/app/tables/page.tsx +21 -0
- package/src/app/tables/templates/page.tsx +19 -0
- package/src/components/chat/chat-table-result.tsx +139 -0
- package/src/components/documents/document-browser.tsx +1 -1
- package/src/components/documents/document-chip-bar.tsx +17 -1
- package/src/components/documents/document-detail-view.tsx +51 -0
- package/src/components/documents/document-grid.tsx +5 -0
- package/src/components/documents/document-table.tsx +4 -0
- package/src/components/documents/types.ts +3 -0
- package/src/components/projects/project-form-sheet.tsx +109 -2
- package/src/components/schedules/schedule-form.tsx +91 -1
- package/src/components/settings/data-management-section.tsx +17 -12
- package/src/components/settings/database-snapshots-section.tsx +469 -0
- package/src/components/shared/app-sidebar.tsx +2 -0
- package/src/components/shared/document-picker-sheet.tsx +486 -0
- package/src/components/tables/table-browser.tsx +234 -0
- package/src/components/tables/table-cell-editor.tsx +226 -0
- package/src/components/tables/table-chart-builder.tsx +288 -0
- package/src/components/tables/table-chart-view.tsx +146 -0
- package/src/components/tables/table-column-header.tsx +103 -0
- package/src/components/tables/table-column-sheet.tsx +331 -0
- package/src/components/tables/table-create-sheet.tsx +240 -0
- package/src/components/tables/table-detail-sheet.tsx +144 -0
- package/src/components/tables/table-detail-tabs.tsx +278 -0
- package/src/components/tables/table-grid.tsx +61 -0
- package/src/components/tables/table-history-tab.tsx +148 -0
- package/src/components/tables/table-import-wizard.tsx +542 -0
- package/src/components/tables/table-list-table.tsx +95 -0
- package/src/components/tables/table-relation-combobox.tsx +217 -0
- package/src/components/tables/table-spreadsheet.tsx +499 -0
- package/src/components/tables/table-template-gallery.tsx +162 -0
- package/src/components/tables/table-template-preview.tsx +219 -0
- package/src/components/tables/table-toolbar.tsx +79 -0
- package/src/components/tables/table-triggers-tab.tsx +446 -0
- package/src/components/tables/types.ts +6 -0
- package/src/components/tables/use-spreadsheet-keys.ts +171 -0
- package/src/components/tables/utils.ts +29 -0
- package/src/components/tasks/task-card.tsx +8 -1
- package/src/components/tasks/task-create-panel.tsx +111 -14
- package/src/components/tasks/task-detail-view.tsx +47 -0
- package/src/components/tasks/task-edit-dialog.tsx +103 -2
- package/src/components/workflows/workflow-form-view.tsx +207 -7
- package/src/components/workflows/workflow-kanban-card.tsx +8 -1
- package/src/components/workflows/workflow-list.tsx +90 -45
- package/src/components/workflows/workflow-status-view.tsx +168 -23
- package/src/instrumentation.ts +3 -0
- package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
- package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
- package/src/lib/agents/claude-agent.ts +3 -1
- package/src/lib/agents/profiles/registry.ts +6 -3
- package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
- package/src/lib/agents/runtime/openai-direct.ts +29 -0
- package/src/lib/book/__tests__/chapter-slugs.test.ts +80 -0
- package/src/lib/book/chapter-generator.ts +4 -19
- package/src/lib/book/chapter-mapping.ts +17 -0
- package/src/lib/book/content.ts +5 -16
- package/src/lib/book/update-detector.ts +3 -16
- package/src/lib/chat/engine.ts +1 -0
- package/src/lib/chat/stagent-tools.ts +2 -0
- package/src/lib/chat/system-prompt.ts +9 -1
- package/src/lib/chat/tool-catalog.ts +35 -0
- package/src/lib/chat/tools/settings-tools.ts +109 -0
- package/src/lib/chat/tools/table-tools.ts +955 -0
- package/src/lib/chat/tools/workflow-tools.ts +145 -2
- package/src/lib/constants/table-status.ts +68 -0
- package/src/lib/data/__tests__/clear.test.ts +1 -1
- package/src/lib/data/clear.ts +57 -0
- package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
- package/src/lib/data/seed-data/conversations.ts +350 -42
- package/src/lib/data/seed-data/documents.ts +564 -591
- package/src/lib/data/seed-data/learned-context.ts +101 -22
- package/src/lib/data/seed-data/notifications.ts +344 -70
- package/src/lib/data/seed-data/profile-test-results.ts +92 -11
- package/src/lib/data/seed-data/profiles.ts +144 -46
- package/src/lib/data/seed-data/projects.ts +50 -18
- package/src/lib/data/seed-data/repo-imports.ts +28 -13
- package/src/lib/data/seed-data/schedules.ts +208 -41
- package/src/lib/data/seed-data/table-templates.ts +234 -0
- package/src/lib/data/seed-data/tasks.ts +614 -116
- package/src/lib/data/seed-data/usage-ledger.ts +182 -103
- package/src/lib/data/seed-data/user-tables.ts +203 -0
- package/src/lib/data/seed-data/views.ts +52 -7
- package/src/lib/data/seed-data/workflows.ts +231 -84
- package/src/lib/data/seed.ts +55 -14
- package/src/lib/data/tables.ts +417 -0
- package/src/lib/db/bootstrap.ts +275 -0
- package/src/lib/db/index.ts +9 -0
- package/src/lib/db/migrations/0016_add_workflow_document_inputs.sql +13 -0
- package/src/lib/db/migrations/0017_add_document_picker_tables.sql +25 -0
- package/src/lib/db/migrations/0018_add_workflow_run_number.sql +2 -0
- package/src/lib/db/migrations/0019_add_tables_feature.sql +160 -0
- package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
- package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
- package/src/lib/db/schema.ts +445 -0
- package/src/lib/docs/reader.ts +2 -3
- package/src/lib/documents/context-builder.ts +75 -2
- package/src/lib/documents/document-resolver.ts +119 -0
- package/src/lib/documents/processors/spreadsheet.ts +2 -1
- package/src/lib/schedules/scheduler.ts +31 -1
- package/src/lib/snapshots/auto-backup.ts +132 -0
- package/src/lib/snapshots/retention.ts +64 -0
- package/src/lib/snapshots/snapshot-manager.ts +429 -0
- package/src/lib/tables/computed.ts +61 -0
- package/src/lib/tables/context-builder.ts +139 -0
- package/src/lib/tables/formula-engine.ts +415 -0
- package/src/lib/tables/history.ts +115 -0
- package/src/lib/tables/import.ts +343 -0
- package/src/lib/tables/query-builder.ts +152 -0
- package/src/lib/tables/trigger-evaluator.ts +146 -0
- package/src/lib/tables/types.ts +141 -0
- package/src/lib/tables/validation.ts +119 -0
- package/src/lib/utils/app-root.ts +20 -0
- package/src/lib/utils/stagent-paths.ts +20 -0
- package/src/lib/validators/__tests__/task.test.ts +43 -10
- package/src/lib/validators/task.ts +7 -1
- package/src/lib/workflows/blueprints/registry.ts +3 -3
- package/src/lib/workflows/engine.ts +24 -8
- package/src/lib/workflows/types.ts +14 -0
- package/tsconfig.json +3 -1
- package/public/icon.svg +0 -13
- package/src/components/tasks/file-upload.tsx +0 -120
- /package/docs/features/{playbook.md → user-guide.md} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getTable, addColumn, getColumns } from "@/lib/data/tables";
|
|
3
|
+
import {
|
|
4
|
+
extractStructuredData,
|
|
5
|
+
inferColumnTypes,
|
|
6
|
+
importRows,
|
|
7
|
+
createImportRecord,
|
|
8
|
+
} from "@/lib/tables/import";
|
|
9
|
+
|
|
10
|
+
export async function POST(
|
|
11
|
+
req: NextRequest,
|
|
12
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
13
|
+
) {
|
|
14
|
+
const { id } = await params;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const table = await getTable(id);
|
|
18
|
+
if (!table) {
|
|
19
|
+
return NextResponse.json({ error: "Table not found" }, { status: 404 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const body = await req.json();
|
|
23
|
+
const { documentId, columnMapping, preview } = body as {
|
|
24
|
+
documentId?: string;
|
|
25
|
+
columnMapping?: Array<{
|
|
26
|
+
name: string;
|
|
27
|
+
displayName: string;
|
|
28
|
+
dataType: string;
|
|
29
|
+
skip?: boolean;
|
|
30
|
+
}>;
|
|
31
|
+
preview?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (!documentId) {
|
|
35
|
+
return NextResponse.json(
|
|
36
|
+
{ error: "documentId is required" },
|
|
37
|
+
{ status: 400 }
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Step 1: Extract structured data from the document
|
|
42
|
+
const { headers, rows } = await extractStructuredData(documentId);
|
|
43
|
+
|
|
44
|
+
// Step 2: Infer column types from a sample (first 100 rows)
|
|
45
|
+
const sampleRows = rows.slice(0, 100);
|
|
46
|
+
const inferredColumns = inferColumnTypes(headers, sampleRows);
|
|
47
|
+
|
|
48
|
+
// If preview mode, return data without importing
|
|
49
|
+
if (preview) {
|
|
50
|
+
return NextResponse.json({
|
|
51
|
+
headers,
|
|
52
|
+
sampleRows: rows.slice(0, 10),
|
|
53
|
+
totalRows: rows.length,
|
|
54
|
+
inferredColumns,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Step 3: Use user-provided column mapping or fall back to inferred
|
|
59
|
+
const finalColumns = columnMapping
|
|
60
|
+
? inferredColumns.map((col) => {
|
|
61
|
+
const mapped = columnMapping.find(
|
|
62
|
+
(m) => m.name === col.name || m.displayName === col.displayName
|
|
63
|
+
);
|
|
64
|
+
if (mapped?.skip) return null;
|
|
65
|
+
if (mapped) {
|
|
66
|
+
return {
|
|
67
|
+
...col,
|
|
68
|
+
name: mapped.name,
|
|
69
|
+
displayName: mapped.displayName,
|
|
70
|
+
dataType: mapped.dataType as typeof col.dataType,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return col;
|
|
74
|
+
}).filter(Boolean) as typeof inferredColumns
|
|
75
|
+
: inferredColumns;
|
|
76
|
+
|
|
77
|
+
// Step 4: Ensure columns exist on the table
|
|
78
|
+
const existingColumns = await getColumns(id);
|
|
79
|
+
const existingNames = new Set(existingColumns.map((c) => c.name));
|
|
80
|
+
|
|
81
|
+
for (const col of finalColumns) {
|
|
82
|
+
if (!existingNames.has(col.name)) {
|
|
83
|
+
await addColumn(id, {
|
|
84
|
+
name: col.name,
|
|
85
|
+
displayName: col.displayName,
|
|
86
|
+
dataType: col.dataType,
|
|
87
|
+
config: col.config,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Step 5: Import all rows
|
|
93
|
+
const result = await importRows(id, rows, finalColumns);
|
|
94
|
+
|
|
95
|
+
// Step 6: Create import audit record
|
|
96
|
+
await createImportRecord(id, documentId, result);
|
|
97
|
+
|
|
98
|
+
return NextResponse.json({
|
|
99
|
+
importId: result.importId,
|
|
100
|
+
rowsImported: result.rowsImported,
|
|
101
|
+
rowsSkipped: result.rowsSkipped,
|
|
102
|
+
errors: result.errors.slice(0, 20), // Limit error detail in response
|
|
103
|
+
columns: finalColumns,
|
|
104
|
+
});
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error("[tables/import] POST error:", err);
|
|
107
|
+
const message =
|
|
108
|
+
err instanceof Error ? err.message : "Failed to import data";
|
|
109
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
110
|
+
}
|
|
111
|
+
}
|