@realtimex/folio 0.1.2
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/.env.example +20 -0
- package/README.md +63 -0
- package/api/server.ts +130 -0
- package/api/src/config/index.ts +96 -0
- package/api/src/middleware/auth.ts +128 -0
- package/api/src/middleware/errorHandler.ts +88 -0
- package/api/src/middleware/index.ts +4 -0
- package/api/src/middleware/rateLimit.ts +71 -0
- package/api/src/middleware/validation.ts +58 -0
- package/api/src/routes/accounts.ts +142 -0
- package/api/src/routes/baseline-config.ts +124 -0
- package/api/src/routes/chat.ts +154 -0
- package/api/src/routes/health.ts +61 -0
- package/api/src/routes/index.ts +35 -0
- package/api/src/routes/ingestions.ts +275 -0
- package/api/src/routes/migrate.ts +112 -0
- package/api/src/routes/policies.ts +121 -0
- package/api/src/routes/processing.ts +90 -0
- package/api/src/routes/rules.ts +11 -0
- package/api/src/routes/sdk.ts +100 -0
- package/api/src/routes/settings.ts +80 -0
- package/api/src/routes/setup.ts +389 -0
- package/api/src/routes/stats.ts +81 -0
- package/api/src/routes/tts.ts +190 -0
- package/api/src/services/BaselineConfigService.ts +208 -0
- package/api/src/services/ChatService.ts +204 -0
- package/api/src/services/GoogleDriveService.ts +331 -0
- package/api/src/services/GoogleSheetsService.ts +1107 -0
- package/api/src/services/IngestionService.ts +1187 -0
- package/api/src/services/ModelCapabilityService.ts +248 -0
- package/api/src/services/PolicyEngine.ts +1625 -0
- package/api/src/services/PolicyLearningService.ts +527 -0
- package/api/src/services/PolicyLoader.ts +249 -0
- package/api/src/services/RAGService.ts +391 -0
- package/api/src/services/SDKService.ts +249 -0
- package/api/src/services/supabase.ts +113 -0
- package/api/src/utils/Actuator.ts +284 -0
- package/api/src/utils/actions/ActionHandler.ts +34 -0
- package/api/src/utils/actions/AppendToGSheetAction.ts +260 -0
- package/api/src/utils/actions/AutoRenameAction.ts +58 -0
- package/api/src/utils/actions/CopyAction.ts +120 -0
- package/api/src/utils/actions/CopyToGDriveAction.ts +64 -0
- package/api/src/utils/actions/LogCsvAction.ts +48 -0
- package/api/src/utils/actions/NotifyAction.ts +39 -0
- package/api/src/utils/actions/RenameAction.ts +57 -0
- package/api/src/utils/actions/WebhookAction.ts +58 -0
- package/api/src/utils/actions/utils.ts +293 -0
- package/api/src/utils/llmResponse.ts +61 -0
- package/api/src/utils/logger.ts +67 -0
- package/bin/folio-deploy.js +12 -0
- package/bin/folio-setup.js +45 -0
- package/bin/folio.js +65 -0
- package/dist/api/server.js +106 -0
- package/dist/api/src/config/index.js +81 -0
- package/dist/api/src/middleware/auth.js +93 -0
- package/dist/api/src/middleware/errorHandler.js +73 -0
- package/dist/api/src/middleware/index.js +4 -0
- package/dist/api/src/middleware/rateLimit.js +43 -0
- package/dist/api/src/middleware/validation.js +54 -0
- package/dist/api/src/routes/accounts.js +110 -0
- package/dist/api/src/routes/baseline-config.js +91 -0
- package/dist/api/src/routes/chat.js +114 -0
- package/dist/api/src/routes/health.js +52 -0
- package/dist/api/src/routes/index.js +31 -0
- package/dist/api/src/routes/ingestions.js +207 -0
- package/dist/api/src/routes/migrate.js +91 -0
- package/dist/api/src/routes/policies.js +86 -0
- package/dist/api/src/routes/processing.js +75 -0
- package/dist/api/src/routes/rules.js +8 -0
- package/dist/api/src/routes/sdk.js +80 -0
- package/dist/api/src/routes/settings.js +68 -0
- package/dist/api/src/routes/setup.js +315 -0
- package/dist/api/src/routes/stats.js +62 -0
- package/dist/api/src/routes/tts.js +178 -0
- package/dist/api/src/services/BaselineConfigService.js +168 -0
- package/dist/api/src/services/ChatService.js +166 -0
- package/dist/api/src/services/GoogleDriveService.js +280 -0
- package/dist/api/src/services/GoogleSheetsService.js +795 -0
- package/dist/api/src/services/IngestionService.js +990 -0
- package/dist/api/src/services/ModelCapabilityService.js +179 -0
- package/dist/api/src/services/PolicyEngine.js +1353 -0
- package/dist/api/src/services/PolicyLearningService.js +397 -0
- package/dist/api/src/services/PolicyLoader.js +159 -0
- package/dist/api/src/services/RAGService.js +295 -0
- package/dist/api/src/services/SDKService.js +212 -0
- package/dist/api/src/services/supabase.js +72 -0
- package/dist/api/src/utils/Actuator.js +225 -0
- package/dist/api/src/utils/actions/ActionHandler.js +1 -0
- package/dist/api/src/utils/actions/AppendToGSheetAction.js +191 -0
- package/dist/api/src/utils/actions/AutoRenameAction.js +49 -0
- package/dist/api/src/utils/actions/CopyAction.js +112 -0
- package/dist/api/src/utils/actions/CopyToGDriveAction.js +55 -0
- package/dist/api/src/utils/actions/LogCsvAction.js +42 -0
- package/dist/api/src/utils/actions/NotifyAction.js +32 -0
- package/dist/api/src/utils/actions/RenameAction.js +51 -0
- package/dist/api/src/utils/actions/WebhookAction.js +51 -0
- package/dist/api/src/utils/actions/utils.js +237 -0
- package/dist/api/src/utils/llmResponse.js +63 -0
- package/dist/api/src/utils/logger.js +51 -0
- package/dist/assets/index-DzN8-j-e.css +1 -0
- package/dist/assets/index-Uy-ai3Dh.js +113 -0
- package/dist/favicon.svg +31 -0
- package/dist/folio-logo.svg +46 -0
- package/dist/index.html +14 -0
- package/docs-dev/FPE-spec.md +196 -0
- package/docs-dev/folio-prd.md +47 -0
- package/docs-dev/foundation-checklist.md +30 -0
- package/docs-dev/hybrid-routing-architecture.md +205 -0
- package/docs-dev/ingestion-engine.md +69 -0
- package/docs-dev/port-from-email-automator.md +32 -0
- package/docs-dev/tech-spec.md +98 -0
- package/index.html +13 -0
- package/package.json +101 -0
- package/public/favicon.svg +31 -0
- package/public/folio-logo.svg +46 -0
- package/scripts/dev-task.mjs +51 -0
- package/scripts/get-latest-migration-timestamp.mjs +34 -0
- package/scripts/migrate.sh +91 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/.temp/gotrue-version +1 -0
- package/supabase/.temp/pooler-url +1 -0
- package/supabase/.temp/postgres-version +1 -0
- package/supabase/.temp/project-ref +1 -0
- package/supabase/.temp/rest-version +1 -0
- package/supabase/.temp/storage-migration +1 -0
- package/supabase/.temp/storage-version +1 -0
- package/supabase/config.toml +64 -0
- package/supabase/functions/_shared/auth.ts +35 -0
- package/supabase/functions/_shared/cors.ts +12 -0
- package/supabase/functions/_shared/supabaseAdmin.ts +17 -0
- package/supabase/functions/api-v1-settings/index.ts +66 -0
- package/supabase/functions/setup/index.ts +91 -0
- package/supabase/migrations/20260223000000_initial_foundation.sql +136 -0
- package/supabase/migrations/20260223000001_add_migration_rpc.sql +10 -0
- package/supabase/migrations/20260224000002_add_init_state_view.sql +20 -0
- package/supabase/migrations/20260224000003_port_user_creation_parity.sql +139 -0
- package/supabase/migrations/20260224000004_add_avatars_storage.sql +26 -0
- package/supabase/migrations/20260224000005_add_tts_and_embed_settings.sql +24 -0
- package/supabase/migrations/20260224000006_add_policies_table.sql +48 -0
- package/supabase/migrations/20260224000007_fix_migration_rpc.sql +9 -0
- package/supabase/migrations/20260224000008_add_ingestions_table.sql +42 -0
- package/supabase/migrations/20260225000000_setup_compatible_mode.sql +119 -0
- package/supabase/migrations/20260225000001_restore_ingestions.sql +49 -0
- package/supabase/migrations/20260225000002_add_ingestion_trace.sql +2 -0
- package/supabase/migrations/20260225000003_add_baseline_configs.sql +35 -0
- package/supabase/migrations/20260226000000_add_processing_events.sql +26 -0
- package/supabase/migrations/20260226000001_add_ingestion_file_hash.sql +10 -0
- package/supabase/migrations/20260226000002_add_dynamic_rag.sql +150 -0
- package/supabase/migrations/20260226000003_add_ingestion_summary.sql +4 -0
- package/supabase/migrations/20260226000004_add_ingestion_tags.sql +7 -0
- package/supabase/migrations/20260226000005_add_chat_tables.sql +60 -0
- package/supabase/migrations/20260227000000_harden_chat_messages_rls.sql +25 -0
- package/supabase/migrations/20260228000000_add_vision_model_capabilities.sql +8 -0
- package/supabase/migrations/20260228000001_add_policy_match_feedback.sql +51 -0
- package/supabase/migrations/29991231235959_test_migration.sql +0 -0
- package/supabase/templates/confirmation.html +76 -0
- package/supabase/templates/email-change.html +76 -0
- package/supabase/templates/invite.html +72 -0
- package/supabase/templates/magic-link.html +68 -0
- package/supabase/templates/recovery.html +82 -0
- package/tsconfig.api.json +16 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +146 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { ActionHandler, ActionContext, ActionResult } from "./ActionHandler.js";
|
|
2
|
+
import { GoogleSheetsService } from "../../services/GoogleSheetsService.js";
|
|
3
|
+
import { pickString, pickColumns, interpolate } from "./utils.js";
|
|
4
|
+
import { Actuator } from "../Actuator.js";
|
|
5
|
+
|
|
6
|
+
const HEADER_ALIASES: Record<string, string[]> = {
|
|
7
|
+
amount: ["total_amount", "amount", "amount_due"],
|
|
8
|
+
total: ["total_amount", "amount", "amount_due"],
|
|
9
|
+
total_amount: ["amount", "amount_due"],
|
|
10
|
+
vendor: ["issuer", "merchant", "store_name", "seller"],
|
|
11
|
+
merchant: ["issuer", "vendor", "store_name", "seller"],
|
|
12
|
+
supplier: ["issuer", "vendor", "merchant"],
|
|
13
|
+
store: ["issuer", "vendor", "merchant", "store_name"],
|
|
14
|
+
document: ["document_type"],
|
|
15
|
+
type: ["document_type"],
|
|
16
|
+
category: ["document_type"],
|
|
17
|
+
issued_on: ["date"],
|
|
18
|
+
invoice_date: ["date"],
|
|
19
|
+
receipt_date: ["date"],
|
|
20
|
+
image_link: ["file_url", "drive_file_url", "file_link", "document_link"],
|
|
21
|
+
file_link: ["file_url", "drive_file_url", "image_link", "document_link"],
|
|
22
|
+
document_link: ["file_url", "drive_file_url", "file_link", "image_link"],
|
|
23
|
+
link: ["file_url", "drive_file_url", "file_link", "document_link", "image_link"],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type HeaderDropdown = {
|
|
27
|
+
strict: boolean;
|
|
28
|
+
allowedValues: string[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function normalizeKey(value: string): string {
|
|
32
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildNormalizedVariableLookup(variables: Record<string, string>): Record<string, string> {
|
|
36
|
+
const lookup: Record<string, string> = {};
|
|
37
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
38
|
+
const normalized = normalizeKey(key);
|
|
39
|
+
if (normalized && !(normalized in lookup)) {
|
|
40
|
+
lookup[normalized] = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return lookup;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveHeaderValue(
|
|
47
|
+
header: string,
|
|
48
|
+
variables: Record<string, string>,
|
|
49
|
+
normalizedVariables: Record<string, string>
|
|
50
|
+
): string {
|
|
51
|
+
const trimmed = header.trim();
|
|
52
|
+
if (!trimmed) return "";
|
|
53
|
+
|
|
54
|
+
if (variables[trimmed] !== undefined) {
|
|
55
|
+
return variables[trimmed];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const normalizedHeader = normalizeKey(trimmed);
|
|
59
|
+
if (!normalizedHeader) return "";
|
|
60
|
+
|
|
61
|
+
if (normalizedVariables[normalizedHeader] !== undefined) {
|
|
62
|
+
return normalizedVariables[normalizedHeader];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const aliases = HEADER_ALIASES[normalizedHeader] ?? [];
|
|
66
|
+
for (const alias of aliases) {
|
|
67
|
+
if (normalizedVariables[alias] !== undefined) {
|
|
68
|
+
return normalizedVariables[alias];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isDropdownValueAllowed(value: string, allowedValues: string[]): boolean {
|
|
76
|
+
const candidate = value.trim().toLowerCase();
|
|
77
|
+
if (!candidate) return true;
|
|
78
|
+
return allowedValues.some((allowed) => allowed.trim().toLowerCase() === candidate);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function applyStrictDropdownGuards(
|
|
82
|
+
headers: string[],
|
|
83
|
+
values: string[],
|
|
84
|
+
headerDropdowns: Array<HeaderDropdown | null>
|
|
85
|
+
): { values: string[]; skippedColumns: string[] } {
|
|
86
|
+
const nextValues = [...values];
|
|
87
|
+
const skippedColumns: string[] = [];
|
|
88
|
+
const maxColumns = Math.min(headers.length, nextValues.length);
|
|
89
|
+
|
|
90
|
+
for (let index = 0; index < maxColumns; index += 1) {
|
|
91
|
+
const dropdown = headerDropdowns[index];
|
|
92
|
+
if (!dropdown || dropdown.strict !== true || dropdown.allowedValues.length === 0) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const currentValue = nextValues[index] ?? "";
|
|
97
|
+
if (!currentValue.trim()) continue;
|
|
98
|
+
if (isDropdownValueAllowed(currentValue, dropdown.allowedValues)) continue;
|
|
99
|
+
|
|
100
|
+
nextValues[index] = "";
|
|
101
|
+
skippedColumns.push(headers[index] || `column_${index + 1}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { values: nextValues, skippedColumns };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class AppendToGSheetAction implements ActionHandler {
|
|
108
|
+
async execute(context: ActionContext): Promise<ActionResult> {
|
|
109
|
+
const { ingestionId, userId, supabase } = context;
|
|
110
|
+
const result: ActionResult = {
|
|
111
|
+
success: true,
|
|
112
|
+
logs: [],
|
|
113
|
+
trace: [],
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const spreadsheetReference =
|
|
117
|
+
pickString(context.action, "spreadsheet_id") ??
|
|
118
|
+
pickString(context.action, "spreadsheet_url");
|
|
119
|
+
|
|
120
|
+
if (!spreadsheetReference) {
|
|
121
|
+
result.success = false;
|
|
122
|
+
result.error = "Missing required Action configuration: 'spreadsheet_id'";
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const configuredRange = pickString(context.action, "range");
|
|
127
|
+
const columnTemplates = pickColumns(context.action, []);
|
|
128
|
+
let rangeToAppend = configuredRange;
|
|
129
|
+
let values: string[] = [];
|
|
130
|
+
let usedDynamicMapping = false;
|
|
131
|
+
|
|
132
|
+
if (columnTemplates.length > 0) {
|
|
133
|
+
values = columnTemplates.map((template) => interpolate(template, context.variables, context.data));
|
|
134
|
+
|
|
135
|
+
const templateResult = await GoogleSheetsService.resolveTemplate(
|
|
136
|
+
context.userId,
|
|
137
|
+
spreadsheetReference,
|
|
138
|
+
configuredRange,
|
|
139
|
+
context.supabase
|
|
140
|
+
);
|
|
141
|
+
if (templateResult.success && (templateResult.headers?.length ?? 0) > 0) {
|
|
142
|
+
const dropdownGuard = applyStrictDropdownGuards(
|
|
143
|
+
templateResult.headers ?? [],
|
|
144
|
+
values,
|
|
145
|
+
(templateResult.headerDropdowns ?? []) as Array<HeaderDropdown | null>
|
|
146
|
+
);
|
|
147
|
+
values = dropdownGuard.values;
|
|
148
|
+
if (dropdownGuard.skippedColumns.length > 0) {
|
|
149
|
+
result.logs.push(
|
|
150
|
+
`Left ${dropdownGuard.skippedColumns.length} strict dropdown column(s) blank for human selection.`
|
|
151
|
+
);
|
|
152
|
+
result.trace.push({
|
|
153
|
+
timestamp: new Date().toISOString(),
|
|
154
|
+
step: "Adjusted strict dropdown columns",
|
|
155
|
+
details: {
|
|
156
|
+
skippedColumns: dropdownGuard.skippedColumns,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
const templateResult = await GoogleSheetsService.resolveTemplate(
|
|
163
|
+
context.userId,
|
|
164
|
+
spreadsheetReference,
|
|
165
|
+
configuredRange,
|
|
166
|
+
context.supabase
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (!templateResult.success) {
|
|
170
|
+
result.success = false;
|
|
171
|
+
result.error = templateResult.error || "Failed to read Google Sheet template headers";
|
|
172
|
+
if (templateResult.errorDetails) {
|
|
173
|
+
result.errorDetails = templateResult.errorDetails;
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const headers = templateResult.headers ?? [];
|
|
179
|
+
if (headers.length === 0) {
|
|
180
|
+
result.success = false;
|
|
181
|
+
result.error = "Google Sheet template has no header row. Add column names in row 1.";
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
rangeToAppend = templateResult.range;
|
|
186
|
+
const normalizedVariables = buildNormalizedVariableLookup(context.variables);
|
|
187
|
+
values = headers.map((header) => resolveHeaderValue(header, context.variables, normalizedVariables));
|
|
188
|
+
const dropdownGuard = applyStrictDropdownGuards(
|
|
189
|
+
headers,
|
|
190
|
+
values,
|
|
191
|
+
(templateResult.headerDropdowns ?? []) as Array<HeaderDropdown | null>
|
|
192
|
+
);
|
|
193
|
+
values = dropdownGuard.values;
|
|
194
|
+
if (dropdownGuard.skippedColumns.length > 0) {
|
|
195
|
+
result.logs.push(
|
|
196
|
+
`Left ${dropdownGuard.skippedColumns.length} strict dropdown column(s) blank for human selection.`
|
|
197
|
+
);
|
|
198
|
+
result.trace.push({
|
|
199
|
+
timestamp: new Date().toISOString(),
|
|
200
|
+
step: "Adjusted strict dropdown columns",
|
|
201
|
+
details: {
|
|
202
|
+
skippedColumns: dropdownGuard.skippedColumns,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
usedDynamicMapping = true;
|
|
207
|
+
|
|
208
|
+
result.trace.push({
|
|
209
|
+
timestamp: new Date().toISOString(),
|
|
210
|
+
step: "Resolved Google Sheet template",
|
|
211
|
+
details: {
|
|
212
|
+
spreadsheetId: templateResult.spreadsheetId,
|
|
213
|
+
range: templateResult.range,
|
|
214
|
+
headersCount: headers.length,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (values.every((value) => value.trim().length === 0)) {
|
|
219
|
+
result.success = false;
|
|
220
|
+
result.error = "Unable to map extracted fields to Google Sheet headers. Provide explicit columns mapping or align header names with extracted keys.";
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
result.trace.push({
|
|
226
|
+
timestamp: new Date().toISOString(),
|
|
227
|
+
step: "Appending to Google Sheet",
|
|
228
|
+
details: { spreadsheetReference, range: rangeToAppend || "Sheet1", columnsCount: values.length, dynamicMapping: usedDynamicMapping },
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const appendResult = await GoogleSheetsService.appendRow(
|
|
232
|
+
context.userId,
|
|
233
|
+
spreadsheetReference,
|
|
234
|
+
rangeToAppend,
|
|
235
|
+
values,
|
|
236
|
+
context.supabase
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (!appendResult.success) {
|
|
240
|
+
result.success = false;
|
|
241
|
+
result.error = appendResult.error;
|
|
242
|
+
if (appendResult.errorDetails) {
|
|
243
|
+
result.errorDetails = appendResult.errorDetails;
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
Actuator.logEvent(ingestionId, userId, "action", "Action Execution", {
|
|
249
|
+
action: "append_to_google_sheet",
|
|
250
|
+
spreadsheetId: appendResult.spreadsheetId ?? spreadsheetReference,
|
|
251
|
+
range: appendResult.range ?? rangeToAppend ?? "Sheet1",
|
|
252
|
+
columnsCount: values.length,
|
|
253
|
+
dynamicMapping: usedDynamicMapping,
|
|
254
|
+
}, supabase);
|
|
255
|
+
|
|
256
|
+
result.logs.push(`Appended ${values.length} columns to Google Sheet ${appendResult.spreadsheetId ?? spreadsheetReference} at ${appendResult.range ?? rangeToAppend ?? "Sheet1"}`);
|
|
257
|
+
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ActionHandler, ActionContext, ActionResult } from "./ActionHandler.js";
|
|
4
|
+
import { resolveFilename } from "./utils.js";
|
|
5
|
+
import { Actuator } from "../Actuator.js";
|
|
6
|
+
import { getServiceRoleSupabase } from "../../services/supabase.js";
|
|
7
|
+
import { createLogger } from "../logger.js";
|
|
8
|
+
|
|
9
|
+
const logger = createLogger("AutoRenameAction");
|
|
10
|
+
|
|
11
|
+
export class AutoRenameAction implements ActionHandler {
|
|
12
|
+
async execute(context: ActionContext): Promise<ActionResult> {
|
|
13
|
+
const { file, variables, data, userId, ingestionId, supabase } = context;
|
|
14
|
+
|
|
15
|
+
const ext = path.extname(file.path);
|
|
16
|
+
const dir = path.dirname(file.path);
|
|
17
|
+
const stem = file.name.slice(0, file.name.length - ext.length);
|
|
18
|
+
|
|
19
|
+
logger.info("AutoRename variables", {
|
|
20
|
+
suggested_filename: variables.suggested_filename ?? "(missing)",
|
|
21
|
+
date: variables.date ?? "(missing)",
|
|
22
|
+
issuer: variables.issuer ?? "(missing)",
|
|
23
|
+
document_type: variables.document_type ?? "(missing)",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const newName = resolveFilename("auto", variables, stem, ext, data);
|
|
27
|
+
const newPath = path.join(dir, newName);
|
|
28
|
+
|
|
29
|
+
await new Promise<void>((resolve, reject) => {
|
|
30
|
+
fs.rename(file.path, newPath, (err) => {
|
|
31
|
+
if (err) reject(err);
|
|
32
|
+
else resolve();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const trace = [{
|
|
37
|
+
timestamp: new Date().toISOString(),
|
|
38
|
+
step: `Auto-Renamed file to ${newName}`,
|
|
39
|
+
details: { original: file.name, new: newName }
|
|
40
|
+
}];
|
|
41
|
+
|
|
42
|
+
logger.info(`AutoRename: '${file.name}' → '${newName}'`);
|
|
43
|
+
Actuator.logEvent(ingestionId, userId, "action", "Action Execution", { action: "auto_rename", original: file.name, new: newName }, supabase);
|
|
44
|
+
|
|
45
|
+
// Update DB so re-runs don't break
|
|
46
|
+
const db = supabase ?? getServiceRoleSupabase();
|
|
47
|
+
if (db) {
|
|
48
|
+
await db.from("ingestions").update({ storage_path: newPath, filename: newName }).eq("id", ingestionId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
newFileState: { path: newPath, name: newName },
|
|
54
|
+
logs: [`Auto-Renamed to '${newName}'`],
|
|
55
|
+
trace
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ActionHandler, ActionContext, ActionResult } from "./ActionHandler.js";
|
|
4
|
+
import { pickString, interpolate, resolveFilename } from "./utils.js";
|
|
5
|
+
import { Actuator } from "../Actuator.js";
|
|
6
|
+
import { GoogleDriveService } from "../../services/GoogleDriveService.js";
|
|
7
|
+
|
|
8
|
+
export class CopyAction implements ActionHandler {
|
|
9
|
+
async execute(context: ActionContext): Promise<ActionResult> {
|
|
10
|
+
const { action, file, variables, data, userId, ingestionId, supabase } = context;
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
const destination = pickString(action as any, "destination");
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
const pattern = pickString(action as any, "pattern");
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
const filenameConfig = pickString(action as any, "filename");
|
|
17
|
+
|
|
18
|
+
if (!destination) {
|
|
19
|
+
return {
|
|
20
|
+
success: false,
|
|
21
|
+
logs: [],
|
|
22
|
+
trace: [{ timestamp: new Date().toISOString(), step: "Copy failed: missing destination" }],
|
|
23
|
+
error: "Copy action requires a 'destination' config"
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const destDir = interpolate(destination, variables, data);
|
|
28
|
+
|
|
29
|
+
// Support legacy gdrive:// destinations before copy_to_gdrive existed.
|
|
30
|
+
if (destDir.startsWith("gdrive://")) {
|
|
31
|
+
const folderPath = destDir.slice("gdrive://".length) || undefined;
|
|
32
|
+
let gdriveFileName: string | undefined;
|
|
33
|
+
if (filenameConfig) {
|
|
34
|
+
const ext = path.extname(file.name);
|
|
35
|
+
const stem = file.name.slice(0, file.name.length - ext.length);
|
|
36
|
+
gdriveFileName = resolveFilename(filenameConfig, variables, stem, ext, data);
|
|
37
|
+
}
|
|
38
|
+
const uploadResult = await GoogleDriveService.uploadFile(userId, file.path, folderPath, supabase, gdriveFileName);
|
|
39
|
+
if (!uploadResult.success) {
|
|
40
|
+
return {
|
|
41
|
+
success: false,
|
|
42
|
+
logs: [],
|
|
43
|
+
trace: [{ timestamp: new Date().toISOString(), step: "Copy to Google Drive failed", details: { error: uploadResult.error } }],
|
|
44
|
+
error: uploadResult.error || "Failed to upload to Google Drive"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const trace = [{
|
|
49
|
+
timestamp: new Date().toISOString(),
|
|
50
|
+
step: "Copied file to Google Drive",
|
|
51
|
+
details: { original: file.path, driveFileId: uploadResult.fileId, destinationFolderId: folderPath }
|
|
52
|
+
}];
|
|
53
|
+
Actuator.logEvent(ingestionId, userId, "action", "Action Execution", { action: "copy_to_gdrive", destinationFolderId: folderPath ?? null, fileId: uploadResult.fileId }, supabase);
|
|
54
|
+
|
|
55
|
+
const fileId = uploadResult.fileId as string;
|
|
56
|
+
const fileUrl = `https://drive.google.com/file/d/${fileId}/view`;
|
|
57
|
+
const outputs: Record<string, unknown> = {
|
|
58
|
+
provider: "google_drive",
|
|
59
|
+
file_id: fileId,
|
|
60
|
+
file_url: fileUrl,
|
|
61
|
+
drive_file_id: fileId,
|
|
62
|
+
drive_file_url: fileUrl,
|
|
63
|
+
destination_folder_id: folderPath ?? null,
|
|
64
|
+
uploaded_file_name: gdriveFileName ?? file.name,
|
|
65
|
+
};
|
|
66
|
+
if (/\.(jpg|jpeg|png|webp|gif|bmp|tiff|tif|heic)$/i.test(file.name)) {
|
|
67
|
+
outputs.image_link = fileUrl;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
success: true,
|
|
72
|
+
logs: [`Copied to Google Drive (ID: ${uploadResult.fileId})`],
|
|
73
|
+
trace,
|
|
74
|
+
outputs,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
79
|
+
|
|
80
|
+
const ext = path.extname(file.name);
|
|
81
|
+
const stem = file.name.slice(0, file.name.length - ext.length);
|
|
82
|
+
let newName: string;
|
|
83
|
+
if (filenameConfig) {
|
|
84
|
+
newName = resolveFilename(filenameConfig, variables, stem, ext, data);
|
|
85
|
+
} else if (pattern) {
|
|
86
|
+
// Backward-compat: treat pattern as a filename template
|
|
87
|
+
newName = interpolate(pattern, variables, data);
|
|
88
|
+
if (!newName.endsWith(ext)) newName += ext;
|
|
89
|
+
} else {
|
|
90
|
+
newName = file.name;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const newPath = path.join(destDir, newName);
|
|
94
|
+
await new Promise<void>((resolve, reject) => {
|
|
95
|
+
fs.copyFile(file.path, newPath, (err) => {
|
|
96
|
+
if (err) reject(err);
|
|
97
|
+
else resolve();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const trace = [{
|
|
102
|
+
timestamp: new Date().toISOString(),
|
|
103
|
+
step: `Copied file to ${newPath}`,
|
|
104
|
+
details: { original: file.path, copy: newPath }
|
|
105
|
+
}];
|
|
106
|
+
Actuator.logEvent(ingestionId, userId, "action", "Action Execution", { action: "copy", destination: destDir, newName }, supabase);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
logs: [`Copied to '${newPath}'`],
|
|
111
|
+
trace,
|
|
112
|
+
outputs: {
|
|
113
|
+
provider: "local",
|
|
114
|
+
destination: destDir,
|
|
115
|
+
copied_path: newPath,
|
|
116
|
+
copied_name: newName,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ActionHandler, ActionContext, ActionResult } from "./ActionHandler.js";
|
|
3
|
+
import { pickString, interpolate, resolveFilename } from "./utils.js";
|
|
4
|
+
import { Actuator } from "../Actuator.js";
|
|
5
|
+
import { GoogleDriveService } from "../../services/GoogleDriveService.js";
|
|
6
|
+
|
|
7
|
+
export class CopyToGDriveAction implements ActionHandler {
|
|
8
|
+
async execute(context: ActionContext): Promise<ActionResult> {
|
|
9
|
+
const { action, file, variables, data, userId, ingestionId, supabase } = context;
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
const destination = pickString(action as any, "destination");
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
const filenameConfig = pickString(action as any, "filename");
|
|
14
|
+
|
|
15
|
+
const destDirId = destination ? interpolate(destination, variables, data) : undefined;
|
|
16
|
+
|
|
17
|
+
let resolvedFileName: string | undefined;
|
|
18
|
+
if (filenameConfig) {
|
|
19
|
+
const ext = path.extname(file.name);
|
|
20
|
+
const stem = file.name.slice(0, file.name.length - ext.length);
|
|
21
|
+
resolvedFileName = resolveFilename(filenameConfig, variables, stem, ext, data);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const uploadResult = await GoogleDriveService.uploadFile(userId, file.path, destDirId, supabase, resolvedFileName);
|
|
25
|
+
|
|
26
|
+
if (!uploadResult.success) {
|
|
27
|
+
return {
|
|
28
|
+
success: false,
|
|
29
|
+
logs: [],
|
|
30
|
+
trace: [{ timestamp: new Date().toISOString(), step: "Copy to Google Drive failed", details: { error: uploadResult.error } }],
|
|
31
|
+
error: uploadResult.error || "Failed to upload to Google Drive"
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const trace = [{
|
|
36
|
+
timestamp: new Date().toISOString(),
|
|
37
|
+
step: `Copied file to Google Drive`,
|
|
38
|
+
details: { original: file.path, driveFileId: uploadResult.fileId, destinationFolderId: destDirId }
|
|
39
|
+
}];
|
|
40
|
+
Actuator.logEvent(ingestionId, userId, "action", "Action Execution", { action: "copy_to_gdrive", destinationFolderId: destDirId ?? null, fileId: uploadResult.fileId }, supabase);
|
|
41
|
+
|
|
42
|
+
const fileId = uploadResult.fileId as string;
|
|
43
|
+
const fileUrl = `https://drive.google.com/file/d/${fileId}/view`;
|
|
44
|
+
const outputs: Record<string, unknown> = {
|
|
45
|
+
provider: "google_drive",
|
|
46
|
+
file_id: fileId,
|
|
47
|
+
file_url: fileUrl,
|
|
48
|
+
drive_file_id: fileId,
|
|
49
|
+
drive_file_url: fileUrl,
|
|
50
|
+
destination_folder_id: destDirId ?? null,
|
|
51
|
+
uploaded_file_name: resolvedFileName ?? file.name,
|
|
52
|
+
};
|
|
53
|
+
if (/\.(jpg|jpeg|png|webp|gif|bmp|tiff|tif|heic)$/i.test(file.name)) {
|
|
54
|
+
outputs.image_link = fileUrl;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
success: true,
|
|
59
|
+
logs: [`Copied to Google Drive (ID: ${uploadResult.fileId})`],
|
|
60
|
+
trace,
|
|
61
|
+
outputs,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ActionHandler, ActionContext, ActionResult } from "./ActionHandler.js";
|
|
4
|
+
import { pickString, pickColumns, interpolate, getNestedVariable } from "./utils.js";
|
|
5
|
+
import { Actuator } from "../Actuator.js";
|
|
6
|
+
|
|
7
|
+
export class LogCsvAction implements ActionHandler {
|
|
8
|
+
async execute(context: ActionContext): Promise<ActionResult> {
|
|
9
|
+
const { action, variables, data, userId, ingestionId, supabase } = context;
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
const csvPathTemplate = pickString(action as any, "path");
|
|
12
|
+
|
|
13
|
+
if (!csvPathTemplate) {
|
|
14
|
+
return {
|
|
15
|
+
success: false,
|
|
16
|
+
logs: [],
|
|
17
|
+
trace: [{ timestamp: new Date().toISOString(), step: "Log CSV failed: missing path" }],
|
|
18
|
+
error: "Log CSV action requires a 'path' config"
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const csvPath = interpolate(csvPathTemplate, variables, data);
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
const cols = pickColumns(action as any, Object.keys(data));
|
|
25
|
+
const row = cols.map((c) => getNestedVariable(c, variables, data) ?? "").join(",") + "\n";
|
|
26
|
+
const header = cols.join(",") + "\n";
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(csvPath)) {
|
|
29
|
+
fs.mkdirSync(path.dirname(csvPath), { recursive: true });
|
|
30
|
+
fs.writeFileSync(csvPath, header + row, "utf-8");
|
|
31
|
+
} else {
|
|
32
|
+
fs.appendFileSync(csvPath, row, "utf-8");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const trace = [{
|
|
36
|
+
timestamp: new Date().toISOString(),
|
|
37
|
+
step: "Executed log_csv action",
|
|
38
|
+
details: { csvPath, cols }
|
|
39
|
+
}];
|
|
40
|
+
Actuator.logEvent(ingestionId, userId, "action", "Action Execution", { action: "log_csv", csvPath, cols }, supabase);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
logs: [`Logged CSV → ${csvPath}`],
|
|
45
|
+
trace
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ActionHandler, ActionContext, ActionResult } from "./ActionHandler.js";
|
|
2
|
+
import { pickString, interpolate } from "./utils.js";
|
|
3
|
+
import { Actuator } from "../Actuator.js";
|
|
4
|
+
import { createLogger } from "../logger.js";
|
|
5
|
+
|
|
6
|
+
const logger = createLogger("NotifyAction");
|
|
7
|
+
|
|
8
|
+
export class NotifyAction implements ActionHandler {
|
|
9
|
+
async execute(context: ActionContext): Promise<ActionResult> {
|
|
10
|
+
const { action, variables, data, userId, ingestionId, supabase } = context;
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
const messageTemplate = pickString(action as any, "message");
|
|
13
|
+
|
|
14
|
+
if (!messageTemplate) {
|
|
15
|
+
return {
|
|
16
|
+
success: false,
|
|
17
|
+
logs: [],
|
|
18
|
+
trace: [{ timestamp: new Date().toISOString(), step: "Notify failed: missing message" }],
|
|
19
|
+
error: "Notify action requires a 'message' config"
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const msg = interpolate(messageTemplate, variables, data);
|
|
24
|
+
logger.info(`[NOTIFY] ${msg}`);
|
|
25
|
+
|
|
26
|
+
const trace = [{
|
|
27
|
+
timestamp: new Date().toISOString(),
|
|
28
|
+
step: "Executed notify action",
|
|
29
|
+
details: { message: msg }
|
|
30
|
+
}];
|
|
31
|
+
Actuator.logEvent(ingestionId, userId, "action", "Action Execution", { action: "notify", message: msg }, supabase);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
success: true,
|
|
35
|
+
logs: [`Notified: ${msg}`],
|
|
36
|
+
trace
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ActionHandler, ActionContext, ActionResult } from "./ActionHandler.js";
|
|
4
|
+
import { pickString, interpolate } from "./utils.js";
|
|
5
|
+
import { Actuator } from "../Actuator.js";
|
|
6
|
+
import { getServiceRoleSupabase } from "../../services/supabase.js";
|
|
7
|
+
|
|
8
|
+
export class RenameAction implements ActionHandler {
|
|
9
|
+
async execute(context: ActionContext): Promise<ActionResult> {
|
|
10
|
+
const { action, file, variables, data, userId, ingestionId, supabase } = context;
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
const pattern = pickString(action as any, "pattern");
|
|
13
|
+
|
|
14
|
+
if (!pattern) {
|
|
15
|
+
return {
|
|
16
|
+
success: false,
|
|
17
|
+
logs: [],
|
|
18
|
+
trace: [{ timestamp: new Date().toISOString(), step: "Rename failed: missing pattern" }],
|
|
19
|
+
error: "Rename action requires a 'pattern' config"
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ext = path.extname(file.path);
|
|
24
|
+
const dir = path.dirname(file.path);
|
|
25
|
+
let newName = interpolate(pattern, variables, data);
|
|
26
|
+
if (!newName.endsWith(ext)) newName += ext;
|
|
27
|
+
const newPath = path.join(dir, newName);
|
|
28
|
+
|
|
29
|
+
await new Promise<void>((resolve, reject) => {
|
|
30
|
+
fs.rename(file.path, newPath, (err) => {
|
|
31
|
+
if (err) reject(err);
|
|
32
|
+
else resolve();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const trace = [{
|
|
37
|
+
timestamp: new Date().toISOString(),
|
|
38
|
+
step: `Renamed file to ${newName}`,
|
|
39
|
+
details: { original: file.name, new: newName }
|
|
40
|
+
}];
|
|
41
|
+
|
|
42
|
+
Actuator.logEvent(ingestionId, userId, "action", "Action Execution", { action: "rename", original: file.name, new: newName }, supabase);
|
|
43
|
+
|
|
44
|
+
// Update DB so re-runs don't break
|
|
45
|
+
const db = supabase ?? getServiceRoleSupabase();
|
|
46
|
+
if (db) {
|
|
47
|
+
await db.from("ingestions").update({ storage_path: newPath, filename: newName }).eq("id", ingestionId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
newFileState: { path: newPath, name: newName },
|
|
53
|
+
logs: [`Renamed to '${newName}'`],
|
|
54
|
+
trace
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|