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,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
|
+
}
|