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.
- package/README.md +21 -2
- package/dist/cli.js +226 -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 -1
- package/src/app/api/chat/entities/search/route.ts +12 -3
- package/src/app/api/projects/[id]/route.ts +37 -0
- package/src/app/api/projects/__tests__/delete-project.test.ts +12 -0
- 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/globals.css +14 -0
- 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/book/book-reader.tsx +62 -9
- package/src/components/book/content-blocks.tsx +6 -1
- package/src/components/chat/chat-table-result.tsx +139 -0
- package/src/components/documents/document-browser.tsx +1 -1
- package/src/components/projects/project-form-sheet.tsx +3 -27
- package/src/components/schedules/schedule-form.tsx +5 -27
- 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 +214 -11
- 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-row-sheet.tsx +271 -0
- package/src/components/tables/table-spreadsheet.tsx +394 -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-create-panel.tsx +5 -31
- package/src/components/tasks/task-edit-dialog.tsx +5 -27
- package/src/components/workflows/workflow-form-view.tsx +11 -35
- package/src/components/workflows/workflow-status-view.tsx +1 -1
- package/src/instrumentation.ts +3 -0
- 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/builtins/document-writer/SKILL.md +23 -0
- package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +10 -0
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +1 -1
- package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
- package/src/lib/agents/runtime/openai-direct.ts +29 -0
- package/src/lib/book/chapter-generator.ts +81 -5
- package/src/lib/book/chapter-mapping.ts +58 -24
- package/src/lib/book/content.ts +83 -47
- package/src/lib/book/markdown-parser.ts +1 -1
- package/src/lib/book/reading-paths.ts +8 -8
- package/src/lib/book/types.ts +1 -1
- package/src/lib/book/update-detector.ts +4 -1
- package/src/lib/chat/stagent-tools.ts +2 -0
- package/src/lib/chat/tool-catalog.ts +34 -0
- package/src/lib/chat/tools/table-tools.ts +955 -0
- package/src/lib/chat/tools/workflow-tools.ts +9 -1
- 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 +45 -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 +227 -0
- package/src/lib/db/index.ts +9 -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 +368 -0
- 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/stagent-paths.ts +20 -0
- package/src/lib/workflows/types.ts +1 -1
- package/tsconfig.json +3 -1
- /package/docs/features/{playbook.md → user-guide.md} +0 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { eq, and, like, or, desc, sql, count } from "drizzle-orm";
|
|
4
|
+
import {
|
|
5
|
+
userTables,
|
|
6
|
+
userTableColumns,
|
|
7
|
+
userTableRows,
|
|
8
|
+
userTableTemplates,
|
|
9
|
+
projects,
|
|
10
|
+
} from "@/lib/db/schema";
|
|
11
|
+
import type {
|
|
12
|
+
CreateTableInput,
|
|
13
|
+
UpdateTableInput,
|
|
14
|
+
AddColumnInput,
|
|
15
|
+
UpdateColumnInput,
|
|
16
|
+
AddRowInput,
|
|
17
|
+
UpdateRowInput,
|
|
18
|
+
ColumnDef,
|
|
19
|
+
RowQueryOptions,
|
|
20
|
+
CloneFromTemplateInput,
|
|
21
|
+
} from "@/lib/tables/types";
|
|
22
|
+
import { buildRowQuery } from "@/lib/tables/query-builder";
|
|
23
|
+
|
|
24
|
+
// ── Table CRUD ───────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export async function createTable(input: CreateTableInput) {
|
|
27
|
+
const id = randomUUID();
|
|
28
|
+
const now = new Date();
|
|
29
|
+
|
|
30
|
+
// Build column schema from input columns
|
|
31
|
+
const columns = (input.columns ?? []).map((col, i) => ({
|
|
32
|
+
...col,
|
|
33
|
+
position: col.position ?? i,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
await db.insert(userTables).values({
|
|
37
|
+
id,
|
|
38
|
+
name: input.name,
|
|
39
|
+
description: input.description ?? null,
|
|
40
|
+
projectId: input.projectId ?? null,
|
|
41
|
+
columnSchema: JSON.stringify(columns),
|
|
42
|
+
rowCount: 0,
|
|
43
|
+
source: input.source ?? "manual",
|
|
44
|
+
templateId: input.templateId ?? null,
|
|
45
|
+
createdAt: now,
|
|
46
|
+
updatedAt: now,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Create individual column records
|
|
50
|
+
if (columns.length > 0) {
|
|
51
|
+
await db.insert(userTableColumns).values(
|
|
52
|
+
columns.map((col) => ({
|
|
53
|
+
id: randomUUID(),
|
|
54
|
+
tableId: id,
|
|
55
|
+
name: col.name,
|
|
56
|
+
displayName: col.displayName,
|
|
57
|
+
dataType: col.dataType,
|
|
58
|
+
position: col.position,
|
|
59
|
+
required: col.required ?? false,
|
|
60
|
+
defaultValue: col.defaultValue ?? null,
|
|
61
|
+
config: col.config ? JSON.stringify(col.config) : null,
|
|
62
|
+
createdAt: now,
|
|
63
|
+
updatedAt: now,
|
|
64
|
+
}))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return db.select().from(userTables).where(eq(userTables.id, id)).get()!;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function getTable(id: string) {
|
|
72
|
+
return db.select().from(userTables).where(eq(userTables.id, id)).get() ?? null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function listTables(filters?: {
|
|
76
|
+
projectId?: string;
|
|
77
|
+
source?: string;
|
|
78
|
+
search?: string;
|
|
79
|
+
}) {
|
|
80
|
+
const conditions = [];
|
|
81
|
+
if (filters?.projectId) {
|
|
82
|
+
conditions.push(eq(userTables.projectId, filters.projectId));
|
|
83
|
+
}
|
|
84
|
+
if (filters?.source) {
|
|
85
|
+
conditions.push(eq(userTables.source, filters.source as "manual" | "imported" | "agent" | "template"));
|
|
86
|
+
}
|
|
87
|
+
if (filters?.search) {
|
|
88
|
+
conditions.push(
|
|
89
|
+
or(
|
|
90
|
+
like(userTables.name, `%${filters.search}%`),
|
|
91
|
+
like(userTables.description, `%${filters.search}%`)
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const rows = db
|
|
97
|
+
.select({
|
|
98
|
+
table: userTables,
|
|
99
|
+
projectName: projects.name,
|
|
100
|
+
})
|
|
101
|
+
.from(userTables)
|
|
102
|
+
.leftJoin(projects, eq(userTables.projectId, projects.id))
|
|
103
|
+
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
|
104
|
+
.orderBy(desc(userTables.updatedAt))
|
|
105
|
+
.all();
|
|
106
|
+
|
|
107
|
+
return rows.map((r) => ({
|
|
108
|
+
...r.table,
|
|
109
|
+
projectName: r.projectName,
|
|
110
|
+
columnCount: parseColumnSchema(r.table.columnSchema).length,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function updateTable(id: string, updates: UpdateTableInput) {
|
|
115
|
+
await db
|
|
116
|
+
.update(userTables)
|
|
117
|
+
.set({ ...updates, updatedAt: new Date() })
|
|
118
|
+
.where(eq(userTables.id, id));
|
|
119
|
+
|
|
120
|
+
return db.select().from(userTables).where(eq(userTables.id, id)).get() ?? null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function deleteTable(id: string) {
|
|
124
|
+
// FK-safe: delete children first
|
|
125
|
+
await db.delete(userTableRows).where(eq(userTableRows.tableId, id));
|
|
126
|
+
await db.delete(userTableColumns).where(eq(userTableColumns.tableId, id));
|
|
127
|
+
await db.delete(userTables).where(eq(userTables.id, id));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Column CRUD ──────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
export async function getColumns(tableId: string) {
|
|
133
|
+
return db
|
|
134
|
+
.select()
|
|
135
|
+
.from(userTableColumns)
|
|
136
|
+
.where(eq(userTableColumns.tableId, tableId))
|
|
137
|
+
.orderBy(userTableColumns.position)
|
|
138
|
+
.all();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function addColumn(tableId: string, input: AddColumnInput) {
|
|
142
|
+
const id = randomUUID();
|
|
143
|
+
const now = new Date();
|
|
144
|
+
|
|
145
|
+
// Get current max position
|
|
146
|
+
const existing = await getColumns(tableId);
|
|
147
|
+
const position = existing.length;
|
|
148
|
+
|
|
149
|
+
await db.insert(userTableColumns).values({
|
|
150
|
+
id,
|
|
151
|
+
tableId,
|
|
152
|
+
name: input.name,
|
|
153
|
+
displayName: input.displayName,
|
|
154
|
+
dataType: input.dataType,
|
|
155
|
+
position,
|
|
156
|
+
required: input.required ?? false,
|
|
157
|
+
defaultValue: input.defaultValue ?? null,
|
|
158
|
+
config: input.config ? JSON.stringify(input.config) : null,
|
|
159
|
+
createdAt: now,
|
|
160
|
+
updatedAt: now,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Update the denormalized column_schema on the table
|
|
164
|
+
await syncColumnSchema(tableId);
|
|
165
|
+
|
|
166
|
+
return db.select().from(userTableColumns).where(eq(userTableColumns.id, id)).get()!;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function updateColumn(columnId: string, input: UpdateColumnInput) {
|
|
170
|
+
const now = new Date();
|
|
171
|
+
const updates: Record<string, unknown> = { updatedAt: now };
|
|
172
|
+
if (input.displayName !== undefined) updates.displayName = input.displayName;
|
|
173
|
+
if (input.dataType !== undefined) updates.dataType = input.dataType;
|
|
174
|
+
if (input.required !== undefined) updates.required = input.required;
|
|
175
|
+
if (input.defaultValue !== undefined) updates.defaultValue = input.defaultValue;
|
|
176
|
+
if (input.config !== undefined) updates.config = input.config ? JSON.stringify(input.config) : null;
|
|
177
|
+
|
|
178
|
+
await db.update(userTableColumns).set(updates).where(eq(userTableColumns.id, columnId));
|
|
179
|
+
|
|
180
|
+
// Get column to find table ID for schema sync
|
|
181
|
+
const col = db.select().from(userTableColumns).where(eq(userTableColumns.id, columnId)).get();
|
|
182
|
+
if (col) await syncColumnSchema(col.tableId);
|
|
183
|
+
|
|
184
|
+
return col ?? null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function deleteColumn(columnId: string) {
|
|
188
|
+
const col = db.select().from(userTableColumns).where(eq(userTableColumns.id, columnId)).get();
|
|
189
|
+
if (!col) return;
|
|
190
|
+
|
|
191
|
+
await db.delete(userTableColumns).where(eq(userTableColumns.id, columnId));
|
|
192
|
+
await syncColumnSchema(col.tableId);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function reorderColumns(tableId: string, columnIds: string[]) {
|
|
196
|
+
const now = new Date();
|
|
197
|
+
for (let i = 0; i < columnIds.length; i++) {
|
|
198
|
+
await db
|
|
199
|
+
.update(userTableColumns)
|
|
200
|
+
.set({ position: i, updatedAt: now })
|
|
201
|
+
.where(eq(userTableColumns.id, columnIds[i]));
|
|
202
|
+
}
|
|
203
|
+
await syncColumnSchema(tableId);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Sync the denormalized column_schema JSON on user_tables from user_table_columns */
|
|
207
|
+
async function syncColumnSchema(tableId: string) {
|
|
208
|
+
const columns = await getColumns(tableId);
|
|
209
|
+
const schema: ColumnDef[] = columns.map((c) => ({
|
|
210
|
+
name: c.name,
|
|
211
|
+
displayName: c.displayName,
|
|
212
|
+
dataType: c.dataType as ColumnDef["dataType"],
|
|
213
|
+
position: c.position,
|
|
214
|
+
required: c.required,
|
|
215
|
+
defaultValue: c.defaultValue,
|
|
216
|
+
config: c.config ? JSON.parse(c.config) : null,
|
|
217
|
+
}));
|
|
218
|
+
|
|
219
|
+
await db
|
|
220
|
+
.update(userTables)
|
|
221
|
+
.set({ columnSchema: JSON.stringify(schema), updatedAt: new Date() })
|
|
222
|
+
.where(eq(userTables.id, tableId));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Row History & Triggers ──────────────────────────────────────────
|
|
226
|
+
import { snapshotBeforeUpdate, snapshotBeforeDelete } from "@/lib/tables/history";
|
|
227
|
+
import { evaluateTriggers } from "@/lib/tables/trigger-evaluator";
|
|
228
|
+
|
|
229
|
+
// ── Row CRUD ─────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
export async function addRows(tableId: string, rows: AddRowInput[]) {
|
|
232
|
+
const now = new Date();
|
|
233
|
+
|
|
234
|
+
// Get current max position
|
|
235
|
+
const maxPos = db
|
|
236
|
+
.select({ maxPos: sql<number>`COALESCE(MAX(position), -1)` })
|
|
237
|
+
.from(userTableRows)
|
|
238
|
+
.where(eq(userTableRows.tableId, tableId))
|
|
239
|
+
.get();
|
|
240
|
+
let nextPosition = (maxPos?.maxPos ?? -1) + 1;
|
|
241
|
+
|
|
242
|
+
const ids: string[] = [];
|
|
243
|
+
for (const row of rows) {
|
|
244
|
+
const id = randomUUID();
|
|
245
|
+
ids.push(id);
|
|
246
|
+
await db.insert(userTableRows).values({
|
|
247
|
+
id,
|
|
248
|
+
tableId,
|
|
249
|
+
data: JSON.stringify(row.data),
|
|
250
|
+
position: nextPosition++,
|
|
251
|
+
createdBy: row.createdBy ?? "user",
|
|
252
|
+
createdAt: now,
|
|
253
|
+
updatedAt: now,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Update denormalized row count
|
|
258
|
+
await updateRowCount(tableId);
|
|
259
|
+
|
|
260
|
+
// Fire triggers for new rows (fire-and-forget)
|
|
261
|
+
for (const [i] of rows.entries()) {
|
|
262
|
+
evaluateTriggers(tableId, "row_added", rows[i].data).catch(() => {});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return ids;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function getRow(rowId: string) {
|
|
269
|
+
return db.select().from(userTableRows).where(eq(userTableRows.id, rowId)).get() ?? null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function listRows(tableId: string, options?: RowQueryOptions) {
|
|
273
|
+
const table = await getTable(tableId);
|
|
274
|
+
if (!table) return [];
|
|
275
|
+
|
|
276
|
+
const columnSchema = parseColumnSchema(table.columnSchema);
|
|
277
|
+
const query = buildRowQuery(columnSchema, options ?? {});
|
|
278
|
+
|
|
279
|
+
const whereClause = query.where
|
|
280
|
+
? and(eq(userTableRows.tableId, tableId), query.where)
|
|
281
|
+
: eq(userTableRows.tableId, tableId);
|
|
282
|
+
|
|
283
|
+
const orderClause = query.orderBy ?? userTableRows.position;
|
|
284
|
+
|
|
285
|
+
return db
|
|
286
|
+
.select()
|
|
287
|
+
.from(userTableRows)
|
|
288
|
+
.where(whereClause)
|
|
289
|
+
.orderBy(orderClause)
|
|
290
|
+
.limit(query.limit)
|
|
291
|
+
.offset(query.offset)
|
|
292
|
+
.all();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function updateRow(rowId: string, input: UpdateRowInput) {
|
|
296
|
+
const existing = await getRow(rowId);
|
|
297
|
+
if (!existing) return null;
|
|
298
|
+
|
|
299
|
+
// Snapshot before mutation (non-blocking)
|
|
300
|
+
try { snapshotBeforeUpdate(rowId, existing.tableId, existing.data, "user"); } catch { /* history is non-critical */ }
|
|
301
|
+
|
|
302
|
+
// Merge new data with existing data
|
|
303
|
+
const existingData = JSON.parse(existing.data) as Record<string, unknown>;
|
|
304
|
+
const mergedData = { ...existingData, ...input.data };
|
|
305
|
+
|
|
306
|
+
await db
|
|
307
|
+
.update(userTableRows)
|
|
308
|
+
.set({
|
|
309
|
+
data: JSON.stringify(mergedData),
|
|
310
|
+
updatedAt: new Date(),
|
|
311
|
+
})
|
|
312
|
+
.where(eq(userTableRows.id, rowId));
|
|
313
|
+
|
|
314
|
+
// Fire triggers (fire-and-forget)
|
|
315
|
+
evaluateTriggers(existing.tableId, "row_updated", mergedData).catch(() => {});
|
|
316
|
+
|
|
317
|
+
return db.select().from(userTableRows).where(eq(userTableRows.id, rowId)).get() ?? null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function deleteRows(rowIds: string[]) {
|
|
321
|
+
if (rowIds.length === 0) return;
|
|
322
|
+
|
|
323
|
+
// Get table ID from first row to update count after
|
|
324
|
+
const firstRow = await getRow(rowIds[0]);
|
|
325
|
+
if (!firstRow) return;
|
|
326
|
+
|
|
327
|
+
for (const id of rowIds) {
|
|
328
|
+
const row = await getRow(id);
|
|
329
|
+
if (row) {
|
|
330
|
+
// Snapshot before deletion (non-blocking)
|
|
331
|
+
try { snapshotBeforeDelete(id, row.tableId, row.data, "user"); } catch { /* history is non-critical */ }
|
|
332
|
+
// Fire trigger before row is gone (fire-and-forget)
|
|
333
|
+
evaluateTriggers(row.tableId, "row_deleted", JSON.parse(row.data) as Record<string, unknown>).catch(() => {});
|
|
334
|
+
}
|
|
335
|
+
await db.delete(userTableRows).where(eq(userTableRows.id, id));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
await updateRowCount(firstRow.tableId);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Update the denormalized row_count on user_tables */
|
|
342
|
+
async function updateRowCount(tableId: string) {
|
|
343
|
+
const result = db
|
|
344
|
+
.select({ total: count() })
|
|
345
|
+
.from(userTableRows)
|
|
346
|
+
.where(eq(userTableRows.tableId, tableId))
|
|
347
|
+
.get();
|
|
348
|
+
|
|
349
|
+
await db
|
|
350
|
+
.update(userTables)
|
|
351
|
+
.set({ rowCount: result?.total ?? 0, updatedAt: new Date() })
|
|
352
|
+
.where(eq(userTables.id, tableId));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ── Template Operations ──────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
export async function listTemplates(filters?: {
|
|
358
|
+
category?: string;
|
|
359
|
+
scope?: string;
|
|
360
|
+
}) {
|
|
361
|
+
const conditions = [];
|
|
362
|
+
if (filters?.category) {
|
|
363
|
+
conditions.push(eq(userTableTemplates.category, filters.category as "business" | "personal" | "pm" | "finance" | "content"));
|
|
364
|
+
}
|
|
365
|
+
if (filters?.scope) {
|
|
366
|
+
conditions.push(eq(userTableTemplates.scope, filters.scope as "system" | "user"));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return db
|
|
370
|
+
.select()
|
|
371
|
+
.from(userTableTemplates)
|
|
372
|
+
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
|
373
|
+
.orderBy(userTableTemplates.name)
|
|
374
|
+
.all();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function getTemplate(id: string) {
|
|
378
|
+
return db.select().from(userTableTemplates).where(eq(userTableTemplates.id, id)).get() ?? null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export async function cloneFromTemplate(input: CloneFromTemplateInput) {
|
|
382
|
+
const template = await getTemplate(input.templateId);
|
|
383
|
+
if (!template) throw new Error("Template not found");
|
|
384
|
+
|
|
385
|
+
const columns = parseColumnSchema(template.columnSchema);
|
|
386
|
+
|
|
387
|
+
const table = await createTable({
|
|
388
|
+
name: input.name,
|
|
389
|
+
projectId: input.projectId,
|
|
390
|
+
columns,
|
|
391
|
+
source: "template",
|
|
392
|
+
templateId: template.id,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Optionally insert sample data
|
|
396
|
+
if (input.includeSampleData && template.sampleData) {
|
|
397
|
+
const sampleRows = JSON.parse(template.sampleData) as Record<string, unknown>[];
|
|
398
|
+
if (sampleRows.length > 0) {
|
|
399
|
+
await addRows(
|
|
400
|
+
table.id,
|
|
401
|
+
sampleRows.map((data) => ({ data }))
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return table;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
function parseColumnSchema(raw: string): ColumnDef[] {
|
|
412
|
+
try {
|
|
413
|
+
return JSON.parse(raw) as ColumnDef[];
|
|
414
|
+
} catch {
|
|
415
|
+
return [];
|
|
416
|
+
}
|
|
417
|
+
}
|
package/src/lib/db/bootstrap.ts
CHANGED
|
@@ -31,6 +31,20 @@ const STAGENT_TABLES = [
|
|
|
31
31
|
"workflow_document_inputs",
|
|
32
32
|
"schedule_document_inputs",
|
|
33
33
|
"project_document_defaults",
|
|
34
|
+
"user_tables",
|
|
35
|
+
"user_table_columns",
|
|
36
|
+
"user_table_rows",
|
|
37
|
+
"user_table_views",
|
|
38
|
+
"user_table_relationships",
|
|
39
|
+
"user_table_templates",
|
|
40
|
+
"user_table_imports",
|
|
41
|
+
"table_document_inputs",
|
|
42
|
+
"task_table_inputs",
|
|
43
|
+
"workflow_table_inputs",
|
|
44
|
+
"schedule_table_inputs",
|
|
45
|
+
"user_table_triggers",
|
|
46
|
+
"user_table_row_history",
|
|
47
|
+
"snapshots",
|
|
34
48
|
] as const;
|
|
35
49
|
|
|
36
50
|
export function bootstrapStagentDatabase(sqlite: Database.Database): void {
|
|
@@ -614,6 +628,219 @@ export function bootstrapStagentDatabase(sqlite: Database.Database): void {
|
|
|
614
628
|
CREATE INDEX IF NOT EXISTS idx_pdd_project ON project_document_defaults(project_id);
|
|
615
629
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_pdd_project_doc ON project_document_defaults(project_id, document_id);
|
|
616
630
|
`);
|
|
631
|
+
|
|
632
|
+
// ── User-Defined Tables (structured data) ──────────────────────────────
|
|
633
|
+
sqlite.exec(`
|
|
634
|
+
CREATE TABLE IF NOT EXISTS user_tables (
|
|
635
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
636
|
+
project_id TEXT,
|
|
637
|
+
name TEXT NOT NULL,
|
|
638
|
+
description TEXT,
|
|
639
|
+
column_schema TEXT NOT NULL DEFAULT '[]',
|
|
640
|
+
row_count INTEGER DEFAULT 0 NOT NULL,
|
|
641
|
+
source TEXT DEFAULT 'manual' NOT NULL,
|
|
642
|
+
template_id TEXT,
|
|
643
|
+
created_at INTEGER NOT NULL,
|
|
644
|
+
updated_at INTEGER NOT NULL,
|
|
645
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
CREATE INDEX IF NOT EXISTS idx_user_tables_project_id ON user_tables(project_id);
|
|
649
|
+
CREATE INDEX IF NOT EXISTS idx_user_tables_source ON user_tables(source);
|
|
650
|
+
|
|
651
|
+
CREATE TABLE IF NOT EXISTS user_table_columns (
|
|
652
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
653
|
+
table_id TEXT NOT NULL,
|
|
654
|
+
name TEXT NOT NULL,
|
|
655
|
+
display_name TEXT NOT NULL,
|
|
656
|
+
data_type TEXT NOT NULL,
|
|
657
|
+
position INTEGER NOT NULL,
|
|
658
|
+
required INTEGER DEFAULT 0 NOT NULL,
|
|
659
|
+
default_value TEXT,
|
|
660
|
+
config TEXT,
|
|
661
|
+
created_at INTEGER NOT NULL,
|
|
662
|
+
updated_at INTEGER NOT NULL,
|
|
663
|
+
FOREIGN KEY (table_id) REFERENCES user_tables(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
CREATE INDEX IF NOT EXISTS idx_user_table_columns_table_id ON user_table_columns(table_id);
|
|
667
|
+
CREATE INDEX IF NOT EXISTS idx_user_table_columns_position ON user_table_columns(table_id, position);
|
|
668
|
+
|
|
669
|
+
CREATE TABLE IF NOT EXISTS user_table_rows (
|
|
670
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
671
|
+
table_id TEXT NOT NULL,
|
|
672
|
+
data TEXT NOT NULL DEFAULT '{}',
|
|
673
|
+
position INTEGER NOT NULL,
|
|
674
|
+
created_by TEXT DEFAULT 'user',
|
|
675
|
+
created_at INTEGER NOT NULL,
|
|
676
|
+
updated_at INTEGER NOT NULL,
|
|
677
|
+
FOREIGN KEY (table_id) REFERENCES user_tables(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
CREATE INDEX IF NOT EXISTS idx_user_table_rows_table_id ON user_table_rows(table_id);
|
|
681
|
+
CREATE INDEX IF NOT EXISTS idx_user_table_rows_position ON user_table_rows(table_id, position);
|
|
682
|
+
|
|
683
|
+
CREATE TABLE IF NOT EXISTS user_table_views (
|
|
684
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
685
|
+
table_id TEXT NOT NULL,
|
|
686
|
+
name TEXT NOT NULL,
|
|
687
|
+
type TEXT DEFAULT 'grid' NOT NULL,
|
|
688
|
+
config TEXT,
|
|
689
|
+
is_default INTEGER DEFAULT 0 NOT NULL,
|
|
690
|
+
created_at INTEGER NOT NULL,
|
|
691
|
+
updated_at INTEGER NOT NULL,
|
|
692
|
+
FOREIGN KEY (table_id) REFERENCES user_tables(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
CREATE INDEX IF NOT EXISTS idx_user_table_views_table_id ON user_table_views(table_id);
|
|
696
|
+
|
|
697
|
+
CREATE TABLE IF NOT EXISTS user_table_relationships (
|
|
698
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
699
|
+
from_table_id TEXT NOT NULL,
|
|
700
|
+
from_column TEXT NOT NULL,
|
|
701
|
+
to_table_id TEXT NOT NULL,
|
|
702
|
+
to_column TEXT NOT NULL,
|
|
703
|
+
relationship_type TEXT NOT NULL,
|
|
704
|
+
config TEXT,
|
|
705
|
+
created_at INTEGER NOT NULL,
|
|
706
|
+
FOREIGN KEY (from_table_id) REFERENCES user_tables(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
707
|
+
FOREIGN KEY (to_table_id) REFERENCES user_tables(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
CREATE INDEX IF NOT EXISTS idx_user_table_rels_from ON user_table_relationships(from_table_id);
|
|
711
|
+
CREATE INDEX IF NOT EXISTS idx_user_table_rels_to ON user_table_relationships(to_table_id);
|
|
712
|
+
|
|
713
|
+
CREATE TABLE IF NOT EXISTS user_table_templates (
|
|
714
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
715
|
+
name TEXT NOT NULL,
|
|
716
|
+
description TEXT,
|
|
717
|
+
category TEXT NOT NULL,
|
|
718
|
+
column_schema TEXT NOT NULL,
|
|
719
|
+
sample_data TEXT,
|
|
720
|
+
scope TEXT DEFAULT 'system' NOT NULL,
|
|
721
|
+
icon TEXT,
|
|
722
|
+
created_at INTEGER NOT NULL,
|
|
723
|
+
updated_at INTEGER NOT NULL
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
CREATE INDEX IF NOT EXISTS idx_user_table_templates_category ON user_table_templates(category);
|
|
727
|
+
CREATE INDEX IF NOT EXISTS idx_user_table_templates_scope ON user_table_templates(scope);
|
|
728
|
+
|
|
729
|
+
CREATE TABLE IF NOT EXISTS user_table_imports (
|
|
730
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
731
|
+
table_id TEXT NOT NULL,
|
|
732
|
+
document_id TEXT,
|
|
733
|
+
row_count INTEGER DEFAULT 0 NOT NULL,
|
|
734
|
+
error_count INTEGER DEFAULT 0 NOT NULL,
|
|
735
|
+
errors TEXT,
|
|
736
|
+
status TEXT DEFAULT 'pending' NOT NULL,
|
|
737
|
+
created_at INTEGER NOT NULL,
|
|
738
|
+
FOREIGN KEY (table_id) REFERENCES user_tables(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
739
|
+
FOREIGN KEY (document_id) REFERENCES documents(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
CREATE INDEX IF NOT EXISTS idx_user_table_imports_table_id ON user_table_imports(table_id);
|
|
743
|
+
|
|
744
|
+
CREATE TABLE IF NOT EXISTS table_document_inputs (
|
|
745
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
746
|
+
table_id TEXT NOT NULL,
|
|
747
|
+
document_id TEXT NOT NULL,
|
|
748
|
+
created_at INTEGER NOT NULL,
|
|
749
|
+
FOREIGN KEY (table_id) REFERENCES user_tables(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
750
|
+
FOREIGN KEY (document_id) REFERENCES documents(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
CREATE INDEX IF NOT EXISTS idx_tdi_table ON table_document_inputs(table_id);
|
|
754
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_tdi_table_doc ON table_document_inputs(table_id, document_id);
|
|
755
|
+
|
|
756
|
+
CREATE TABLE IF NOT EXISTS task_table_inputs (
|
|
757
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
758
|
+
task_id TEXT NOT NULL,
|
|
759
|
+
table_id TEXT NOT NULL,
|
|
760
|
+
created_at INTEGER NOT NULL,
|
|
761
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
762
|
+
FOREIGN KEY (table_id) REFERENCES user_tables(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
CREATE INDEX IF NOT EXISTS idx_tti_task ON task_table_inputs(task_id);
|
|
766
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_tti_task_table ON task_table_inputs(task_id, table_id);
|
|
767
|
+
|
|
768
|
+
CREATE TABLE IF NOT EXISTS workflow_table_inputs (
|
|
769
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
770
|
+
workflow_id TEXT NOT NULL,
|
|
771
|
+
table_id TEXT NOT NULL,
|
|
772
|
+
step_id TEXT,
|
|
773
|
+
created_at INTEGER NOT NULL,
|
|
774
|
+
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
775
|
+
FOREIGN KEY (table_id) REFERENCES user_tables(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
CREATE INDEX IF NOT EXISTS idx_wti_workflow ON workflow_table_inputs(workflow_id);
|
|
779
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_wti_workflow_table_step ON workflow_table_inputs(workflow_id, table_id, step_id);
|
|
780
|
+
|
|
781
|
+
CREATE TABLE IF NOT EXISTS schedule_table_inputs (
|
|
782
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
783
|
+
schedule_id TEXT NOT NULL,
|
|
784
|
+
table_id TEXT NOT NULL,
|
|
785
|
+
created_at INTEGER NOT NULL,
|
|
786
|
+
FOREIGN KEY (schedule_id) REFERENCES schedules(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
787
|
+
FOREIGN KEY (table_id) REFERENCES user_tables(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
CREATE INDEX IF NOT EXISTS idx_sti_schedule ON schedule_table_inputs(schedule_id);
|
|
791
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_sti_schedule_table ON schedule_table_inputs(schedule_id, table_id);
|
|
792
|
+
|
|
793
|
+
CREATE TABLE IF NOT EXISTS user_table_triggers (
|
|
794
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
795
|
+
table_id TEXT NOT NULL,
|
|
796
|
+
name TEXT NOT NULL,
|
|
797
|
+
trigger_event TEXT NOT NULL,
|
|
798
|
+
condition TEXT,
|
|
799
|
+
action_type TEXT NOT NULL,
|
|
800
|
+
action_config TEXT NOT NULL,
|
|
801
|
+
status TEXT DEFAULT 'active' NOT NULL,
|
|
802
|
+
fire_count INTEGER DEFAULT 0 NOT NULL,
|
|
803
|
+
last_fired_at INTEGER,
|
|
804
|
+
created_at INTEGER NOT NULL,
|
|
805
|
+
updated_at INTEGER NOT NULL,
|
|
806
|
+
FOREIGN KEY (table_id) REFERENCES user_tables(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
CREATE INDEX IF NOT EXISTS idx_user_table_triggers_table_id ON user_table_triggers(table_id);
|
|
810
|
+
CREATE INDEX IF NOT EXISTS idx_user_table_triggers_status ON user_table_triggers(status);
|
|
811
|
+
|
|
812
|
+
CREATE TABLE IF NOT EXISTS user_table_row_history (
|
|
813
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
814
|
+
row_id TEXT NOT NULL,
|
|
815
|
+
table_id TEXT NOT NULL,
|
|
816
|
+
previous_data TEXT NOT NULL,
|
|
817
|
+
changed_by TEXT DEFAULT 'user',
|
|
818
|
+
change_type TEXT NOT NULL,
|
|
819
|
+
created_at INTEGER NOT NULL,
|
|
820
|
+
FOREIGN KEY (table_id) REFERENCES user_tables(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
CREATE INDEX IF NOT EXISTS idx_row_history_row_id ON user_table_row_history(row_id);
|
|
824
|
+
CREATE INDEX IF NOT EXISTS idx_row_history_table_id ON user_table_row_history(table_id);
|
|
825
|
+
CREATE INDEX IF NOT EXISTS idx_row_history_created_at ON user_table_row_history(created_at);
|
|
826
|
+
|
|
827
|
+
CREATE TABLE IF NOT EXISTS snapshots (
|
|
828
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
829
|
+
label TEXT NOT NULL,
|
|
830
|
+
type TEXT DEFAULT 'manual' NOT NULL,
|
|
831
|
+
status TEXT DEFAULT 'in_progress' NOT NULL,
|
|
832
|
+
file_path TEXT NOT NULL,
|
|
833
|
+
size_bytes INTEGER DEFAULT 0 NOT NULL,
|
|
834
|
+
db_size_bytes INTEGER DEFAULT 0 NOT NULL,
|
|
835
|
+
files_size_bytes INTEGER DEFAULT 0 NOT NULL,
|
|
836
|
+
file_count INTEGER DEFAULT 0 NOT NULL,
|
|
837
|
+
error TEXT,
|
|
838
|
+
created_at INTEGER NOT NULL
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_type ON snapshots(type);
|
|
842
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_created_at ON snapshots(created_at);
|
|
843
|
+
`);
|
|
617
844
|
}
|
|
618
845
|
|
|
619
846
|
export function hasLegacyStagentTables(sqlite: Database.Database): boolean {
|
package/src/lib/db/index.ts
CHANGED
|
@@ -16,3 +16,12 @@ sqlite.pragma("foreign_keys = ON");
|
|
|
16
16
|
bootstrapStagentDatabase(sqlite);
|
|
17
17
|
|
|
18
18
|
export const db = drizzle(sqlite, { schema });
|
|
19
|
+
export { sqlite };
|
|
20
|
+
|
|
21
|
+
// Lazy seed: table templates (idempotent — checks before inserting)
|
|
22
|
+
import("@/lib/data/seed-data/table-templates").then(({ seedTableTemplates }) => {
|
|
23
|
+
seedTableTemplates().catch(() => {
|
|
24
|
+
// Template seeding is non-critical — log and continue
|
|
25
|
+
console.warn("[db] table template seeding failed");
|
|
26
|
+
});
|
|
27
|
+
});
|