@gobing-ai/ts-llm-jsonl-importer 0.4.19 → 0.4.21
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/dist/importer.d.ts.map +1 -1
- package/dist/importer.js +32 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/jsonl-importer-dao.d.ts +6 -6
- package/dist/jsonl-importer-dao.d.ts.map +1 -1
- package/dist/jsonl-importer-dao.js +95 -9
- package/dist/mappers.d.ts +41 -0
- package/dist/mappers.d.ts.map +1 -0
- package/dist/mappers.js +1035 -0
- package/dist/schema-sql.d.ts.map +1 -1
- package/dist/schema-sql.js +57 -0
- package/dist/sources.d.ts.map +1 -1
- package/dist/sources.js +23 -5
- package/dist/types.d.ts +8 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/importer.ts +41 -5
- package/src/index.ts +1 -0
- package/src/jsonl-importer-dao.ts +108 -17
- package/src/mappers.ts +1130 -0
- package/src/schema-sql.ts +57 -0
- package/src/sources.ts +92 -5
- package/src/types.ts +19 -2
package/dist/schema-sql.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-sql.d.ts","sourceRoot":"","sources":["../src/schema-sql.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,eAAO,MAAM,yBAAyB,
|
|
1
|
+
{"version":3,"file":"schema-sql.d.ts","sourceRoot":"","sources":["../src/schema-sql.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,eAAO,MAAM,yBAAyB,QA0I9B,CAAC"}
|
package/dist/schema-sql.js
CHANGED
|
@@ -80,4 +80,61 @@ CREATE TABLE IF NOT EXISTS history_etl_openclaw (
|
|
|
80
80
|
payload_json TEXT NOT NULL,
|
|
81
81
|
imported_at TEXT NOT NULL
|
|
82
82
|
);
|
|
83
|
+
|
|
84
|
+
CREATE TABLE IF NOT EXISTS history_message (
|
|
85
|
+
record_hash TEXT PRIMARY KEY,
|
|
86
|
+
source TEXT NOT NULL,
|
|
87
|
+
source_file TEXT NOT NULL,
|
|
88
|
+
source_line INTEGER NOT NULL,
|
|
89
|
+
session_id TEXT NOT NULL,
|
|
90
|
+
seq INTEGER NOT NULL,
|
|
91
|
+
turn_index INTEGER,
|
|
92
|
+
role TEXT NOT NULL,
|
|
93
|
+
record_type TEXT NOT NULL,
|
|
94
|
+
disposition TEXT NOT NULL,
|
|
95
|
+
ts TEXT NOT NULL,
|
|
96
|
+
duration_ms INTEGER,
|
|
97
|
+
model TEXT,
|
|
98
|
+
input_tokens INTEGER,
|
|
99
|
+
output_tokens INTEGER,
|
|
100
|
+
cache_read_tokens INTEGER,
|
|
101
|
+
cache_write_tokens INTEGER,
|
|
102
|
+
cost_usd REAL,
|
|
103
|
+
content_text TEXT,
|
|
104
|
+
cwd TEXT,
|
|
105
|
+
provenance TEXT NOT NULL,
|
|
106
|
+
run_id TEXT,
|
|
107
|
+
task_wbs TEXT,
|
|
108
|
+
imported_at TEXT NOT NULL
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
CREATE TABLE IF NOT EXISTS history_tool_call (
|
|
112
|
+
record_hash TEXT PRIMARY KEY,
|
|
113
|
+
message_hash TEXT NOT NULL,
|
|
114
|
+
source TEXT NOT NULL,
|
|
115
|
+
source_file TEXT NOT NULL,
|
|
116
|
+
source_line INTEGER NOT NULL,
|
|
117
|
+
session_id TEXT NOT NULL,
|
|
118
|
+
seq INTEGER NOT NULL,
|
|
119
|
+
tool_name TEXT NOT NULL,
|
|
120
|
+
args_digest TEXT,
|
|
121
|
+
status TEXT NOT NULL,
|
|
122
|
+
started_at TEXT,
|
|
123
|
+
completed_at TEXT,
|
|
124
|
+
duration_ms INTEGER,
|
|
125
|
+
result_bytes INTEGER,
|
|
126
|
+
error_text TEXT,
|
|
127
|
+
imported_at TEXT NOT NULL
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
CREATE INDEX IF NOT EXISTS idx_history_message_session
|
|
131
|
+
ON history_message(source, session_id, seq);
|
|
132
|
+
CREATE INDEX IF NOT EXISTS idx_history_message_ts
|
|
133
|
+
ON history_message(ts);
|
|
134
|
+
CREATE INDEX IF NOT EXISTS idx_history_tool_call_session
|
|
135
|
+
ON history_tool_call(source, session_id, seq);
|
|
136
|
+
CREATE INDEX IF NOT EXISTS idx_history_tool_call_tool_name
|
|
137
|
+
ON history_tool_call(tool_name);
|
|
138
|
+
CREATE INDEX IF NOT EXISTS idx_history_tool_call_message_hash
|
|
139
|
+
ON history_tool_call(message_hash);
|
|
83
140
|
`.trim();
|
package/dist/sources.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["../src/sources.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["../src/sources.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAA8B,cAAc,EAAE,gBAAgB,EAAoB,MAAM,SAAS,CAAC;AAsH9G;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,QAAsB,CAAC;AAEpD,8DAA8D;AAC9D,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,gBAAgB,CAAC,CAmDjF,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,GAAG,gBAAgB,CAM5E;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,gBAAgB,GAAG,gBAAgB,CA+BvF;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,gBAAgB,CAK1F"}
|
package/dist/sources.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { HistoryImportError } from './errors.js';
|
|
3
|
+
import { AGY_FIELD_MAP, AGY_SCHEMA, agySplit, CLAUDE_FIELD_MAP, CLAUDE_SCHEMA, CODEX_FIELD_MAP, CODEX_SCHEMA, claudeSplit, codexSplit, GROK_FIELD_MAP, GROK_SCHEMA, grokSplit, OMP_FIELD_MAP, OMP_SCHEMA, ompSplit, PI_FIELD_MAP, PI_SCHEMA, piSplit, } from './mappers.js';
|
|
3
4
|
const sourceRecordSchema = z
|
|
4
5
|
.object({
|
|
5
6
|
source_record_id: z.string().min(1),
|
|
@@ -72,6 +73,20 @@ function sourceDefinition(source, displayName, defaultRoots, filePatterns) {
|
|
|
72
73
|
schema: sourceRecordSchema,
|
|
73
74
|
};
|
|
74
75
|
}
|
|
76
|
+
/** Build a source definition with a typed custom split for the forensic contract tables. */
|
|
77
|
+
function customSourceDefinition(source, displayName, defaultRoots, filePatterns, split, fieldMap, schema) {
|
|
78
|
+
return {
|
|
79
|
+
source,
|
|
80
|
+
displayName,
|
|
81
|
+
defaultRoots,
|
|
82
|
+
filePatterns,
|
|
83
|
+
targetTable: `history_etl_${source}`,
|
|
84
|
+
splitConfig: { mode: 'custom', split },
|
|
85
|
+
fieldMap,
|
|
86
|
+
fieldTransforms: {},
|
|
87
|
+
schema,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
75
90
|
/**
|
|
76
91
|
* Pattern every importer-owned table must match.
|
|
77
92
|
*
|
|
@@ -80,12 +95,15 @@ function sourceDefinition(source, displayName, defaultRoots, filePatterns) {
|
|
|
80
95
|
* namespaces importer tables away from consumer schemas. Both built-in and
|
|
81
96
|
* custom source definitions are held to this rule.
|
|
82
97
|
*/
|
|
83
|
-
export const VALID_TABLE_NAME = /^
|
|
98
|
+
export const VALID_TABLE_NAME = /^history_[a-z_]+$/;
|
|
84
99
|
/** Built-in source definitions keyed by source identifier. */
|
|
85
100
|
export const SOURCE_DEFINITIONS = {
|
|
86
|
-
pi:
|
|
87
|
-
claude:
|
|
88
|
-
codex:
|
|
101
|
+
pi: customSourceDefinition('pi', 'Pi', ['.pi/agent/sessions'], ['*.jsonl'], piSplit, PI_FIELD_MAP, PI_SCHEMA),
|
|
102
|
+
claude: customSourceDefinition('claude', 'Claude Code', ['.claude/projects'], ['*.jsonl'], claudeSplit, CLAUDE_FIELD_MAP, CLAUDE_SCHEMA),
|
|
103
|
+
codex: customSourceDefinition('codex', 'Codex', ['.codex/sessions'], ['*.jsonl'], codexSplit, CODEX_FIELD_MAP, CODEX_SCHEMA),
|
|
104
|
+
omp: customSourceDefinition('omp', 'OMP', ['.omp/agent/sessions'], ['*.jsonl'], ompSplit, OMP_FIELD_MAP, OMP_SCHEMA),
|
|
105
|
+
grok: customSourceDefinition('grok', 'Grok', ['.grok/sessions'], ['*.jsonl'], grokSplit, GROK_FIELD_MAP, GROK_SCHEMA),
|
|
106
|
+
agy: customSourceDefinition('agy', 'Antigravity CLI', ['.gemini/antigravity-cli/brain'], ['*.jsonl'], agySplit, AGY_FIELD_MAP, AGY_SCHEMA),
|
|
89
107
|
gemini: sourceDefinition('gemini', 'Gemini CLI', ['.gemini', '.config/gemini'], ['*.jsonl']),
|
|
90
108
|
opencode: sourceDefinition('opencode', 'OpenCode', ['.opencode', '.local/share/opencode'], ['*.jsonl']),
|
|
91
109
|
antigravity: sourceDefinition('antigravity', 'Antigravity', ['.antigravity'], ['*.jsonl']),
|
|
@@ -158,6 +176,6 @@ export function resolveSourceDefinition(input) {
|
|
|
158
176
|
}
|
|
159
177
|
function assertValidTable(table, source) {
|
|
160
178
|
if (!VALID_TABLE_NAME.test(table)) {
|
|
161
|
-
throw new HistoryImportError(`Invalid history
|
|
179
|
+
throw new HistoryImportError(`Invalid history target table for source "${source}": ${table}. Must match ${VALID_TABLE_NAME.source}.`, { source, table });
|
|
162
180
|
}
|
|
163
181
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -2,9 +2,14 @@ import type { DbAdapter } from '@gobing-ai/ts-db';
|
|
|
2
2
|
import type { FileSystem, RuntimePaths } from '@gobing-ai/ts-runtime';
|
|
3
3
|
import type { z } from 'zod';
|
|
4
4
|
/** Built-in source identifiers supported by the importer. */
|
|
5
|
-
export type LlmJsonlSource = 'pi' | 'claude' | 'codex' | 'gemini' | 'opencode' | 'antigravity' | 'openclaw';
|
|
5
|
+
export type LlmJsonlSource = 'pi' | 'claude' | 'codex' | 'gemini' | 'opencode' | 'antigravity' | 'openclaw' | 'omp' | 'grok' | 'agy';
|
|
6
6
|
/** Import mode controlling checkpoint behavior. */
|
|
7
7
|
export type ImportMode = 'full' | 'incremental' | 'force-file';
|
|
8
|
+
/** One record emitted by a custom split, optionally routed to its own table. */
|
|
9
|
+
export interface SplitEntry {
|
|
10
|
+
readonly targetTable?: string;
|
|
11
|
+
readonly record: JsonObject;
|
|
12
|
+
}
|
|
8
13
|
/** Split strategy for turning a raw JSONL object into one or more ETL records. */
|
|
9
14
|
export type SplitConfig = {
|
|
10
15
|
readonly mode: 'one-to-one';
|
|
@@ -14,7 +19,7 @@ export type SplitConfig = {
|
|
|
14
19
|
readonly targetTable?: string;
|
|
15
20
|
} | {
|
|
16
21
|
readonly mode: 'custom';
|
|
17
|
-
readonly split: (raw: JsonObject) => readonly JsonObject[];
|
|
22
|
+
readonly split: (raw: JsonObject) => readonly (JsonObject | SplitEntry)[];
|
|
18
23
|
readonly targetTable?: string;
|
|
19
24
|
};
|
|
20
25
|
/** JSON-compatible record used at the importer boundary. */
|
|
@@ -80,6 +85,7 @@ export interface ImportResult {
|
|
|
80
85
|
readonly processedLines: number;
|
|
81
86
|
readonly importedRecords: number;
|
|
82
87
|
readonly skippedDuplicates: number;
|
|
88
|
+
readonly unknownRecords: number;
|
|
83
89
|
readonly parseErrors: readonly ImportIssue[];
|
|
84
90
|
readonly validationErrors: readonly ImportIssue[];
|
|
85
91
|
readonly checkpointUpdates: number;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,6DAA6D;AAC7D,MAAM,MAAM,cAAc,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-llm-jsonl-importer",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.21",
|
|
4
4
|
"description": "@gobing-ai/ts-llm-jsonl-importer — Generic JSONL importer for LLM agent history files.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-llm-jsonl-importer-v<version> && git push --tags' && exit 1"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@gobing-ai/ts-db": "^0.4.
|
|
51
|
-
"@gobing-ai/ts-runtime": "^0.4.
|
|
52
|
-
"@gobing-ai/ts-utils": "^0.4.
|
|
50
|
+
"@gobing-ai/ts-db": "^0.4.21",
|
|
51
|
+
"@gobing-ai/ts-runtime": "^0.4.21",
|
|
52
|
+
"@gobing-ai/ts-utils": "^0.4.21",
|
|
53
53
|
"zod": "^4.1.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
package/src/importer.ts
CHANGED
|
@@ -53,6 +53,7 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
|
|
|
53
53
|
let processedLines = 0;
|
|
54
54
|
let importedRecords = 0;
|
|
55
55
|
let skippedDuplicates = 0;
|
|
56
|
+
let unknownRecords = 0;
|
|
56
57
|
let checkpointUpdates = 0;
|
|
57
58
|
|
|
58
59
|
for (const file of files) {
|
|
@@ -70,6 +71,14 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
|
|
|
70
71
|
const splitRecords = splitRawRecord(definition, raw);
|
|
71
72
|
let lineSucceeded = false;
|
|
72
73
|
|
|
74
|
+
// First pass: normalize, validate, redact, compute recordHash for every entry.
|
|
75
|
+
interface PreparedEntry {
|
|
76
|
+
readonly split: SplitRecord;
|
|
77
|
+
readonly splitIndex: number;
|
|
78
|
+
readonly normalized: JsonObject;
|
|
79
|
+
readonly recordHash: string;
|
|
80
|
+
}
|
|
81
|
+
const prepared: PreparedEntry[] = [];
|
|
73
82
|
for (let splitIndex = 0; splitIndex < splitRecords.length; splitIndex += 1) {
|
|
74
83
|
const split = splitRecords[splitIndex];
|
|
75
84
|
if (split === undefined) continue;
|
|
@@ -102,6 +111,25 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
|
|
|
102
111
|
skippedDuplicates += 1;
|
|
103
112
|
continue;
|
|
104
113
|
}
|
|
114
|
+
prepared.push({ split, splitIndex, normalized: redacted, recordHash });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Second pass: resolve _messageSplitIndex → message_hash, then insert.
|
|
118
|
+
const recordHashBySplitIndex = new Map(prepared.map((e) => [e.splitIndex, e.recordHash] as const));
|
|
119
|
+
for (const entry of prepared) {
|
|
120
|
+
const { split, splitIndex, normalized, recordHash } = entry;
|
|
121
|
+
if (normalized.disposition === 'unknown') {
|
|
122
|
+
unknownRecords += 1;
|
|
123
|
+
}
|
|
124
|
+
// Resolve _messageSplitIndex if present (tool call → parent message linkage).
|
|
125
|
+
const msgSplitIdx = normalized._messageSplitIndex as number | undefined;
|
|
126
|
+
delete normalized._messageSplitIndex;
|
|
127
|
+
if (msgSplitIdx !== undefined && typeof msgSplitIdx === 'number') {
|
|
128
|
+
const msgHash = recordHashBySplitIndex.get(msgSplitIdx);
|
|
129
|
+
if (msgHash !== undefined) {
|
|
130
|
+
normalized.message_hash = msgHash;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
105
133
|
if (!options.dryRun) {
|
|
106
134
|
await insertRecord(
|
|
107
135
|
options.db,
|
|
@@ -110,7 +138,7 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
|
|
|
110
138
|
file,
|
|
111
139
|
lineNumber,
|
|
112
140
|
splitIndex,
|
|
113
|
-
|
|
141
|
+
normalized,
|
|
114
142
|
options.now,
|
|
115
143
|
);
|
|
116
144
|
await insertLedger(
|
|
@@ -141,6 +169,7 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
|
|
|
141
169
|
processedLines,
|
|
142
170
|
importedRecords,
|
|
143
171
|
skippedDuplicates,
|
|
172
|
+
unknownRecords,
|
|
144
173
|
parseErrors,
|
|
145
174
|
validationErrors,
|
|
146
175
|
checkpointUpdates,
|
|
@@ -173,10 +202,17 @@ function splitRawRecord(definition: SourceDefinition, raw: JsonObject): readonly
|
|
|
173
202
|
}
|
|
174
203
|
if (definition.splitConfig.mode === 'custom') {
|
|
175
204
|
const config = definition.splitConfig;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
205
|
+
const configTable = config.targetTable !== undefined ? targetTableFor(config.targetTable) : undefined;
|
|
206
|
+
return config.split(raw).map((entry) => {
|
|
207
|
+
// Normalize: SplitEntry has a `record` property, bare object is the record itself.
|
|
208
|
+
const hasTargetTable =
|
|
209
|
+
'targetTable' in entry && typeof (entry as { targetTable: unknown }).targetTable === 'string';
|
|
210
|
+
const record = hasTargetTable ? (entry as { record: JsonObject }).record : (entry as JsonObject);
|
|
211
|
+
const entryTable = hasTargetTable ? (entry as { targetTable?: string }).targetTable : undefined;
|
|
212
|
+
// Resolution order: entry → splitConfig → definition
|
|
213
|
+
const table = entryTable ?? configTable ?? targetTable;
|
|
214
|
+
return { targetTable: targetTableFor(table), raw: record };
|
|
215
|
+
});
|
|
180
216
|
}
|
|
181
217
|
|
|
182
218
|
const config = definition.splitConfig;
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,57 @@ interface CheckpointRow {
|
|
|
7
7
|
readonly last_imported_line: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/** Column allowlist per typed contract table; order is the INSERT column order. */
|
|
11
|
+
const TYPED_TABLE_COLUMNS: Readonly<Record<string, readonly string[]>> = {
|
|
12
|
+
history_message: [
|
|
13
|
+
'record_hash',
|
|
14
|
+
'source',
|
|
15
|
+
'source_file',
|
|
16
|
+
'source_line',
|
|
17
|
+
'session_id',
|
|
18
|
+
'seq',
|
|
19
|
+
'turn_index',
|
|
20
|
+
'role',
|
|
21
|
+
'record_type',
|
|
22
|
+
'disposition',
|
|
23
|
+
'ts',
|
|
24
|
+
'duration_ms',
|
|
25
|
+
'model',
|
|
26
|
+
'input_tokens',
|
|
27
|
+
'output_tokens',
|
|
28
|
+
'cache_read_tokens',
|
|
29
|
+
'cache_write_tokens',
|
|
30
|
+
'cost_usd',
|
|
31
|
+
'content_text',
|
|
32
|
+
'cwd',
|
|
33
|
+
'provenance',
|
|
34
|
+
'run_id',
|
|
35
|
+
'task_wbs',
|
|
36
|
+
'imported_at',
|
|
37
|
+
],
|
|
38
|
+
history_tool_call: [
|
|
39
|
+
'record_hash',
|
|
40
|
+
'message_hash',
|
|
41
|
+
'source',
|
|
42
|
+
'source_file',
|
|
43
|
+
'source_line',
|
|
44
|
+
'session_id',
|
|
45
|
+
'seq',
|
|
46
|
+
'tool_name',
|
|
47
|
+
'args_digest',
|
|
48
|
+
'status',
|
|
49
|
+
'started_at',
|
|
50
|
+
'completed_at',
|
|
51
|
+
'duration_ms',
|
|
52
|
+
'result_bytes',
|
|
53
|
+
'error_text',
|
|
54
|
+
'imported_at',
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Keys that a typed table mapper may produce that are not columns. */
|
|
59
|
+
const TYPED_IGNORED_KEYS = new Set<string>(['_meta', 'split_index']);
|
|
60
|
+
|
|
10
61
|
function targetTableFor(table: string): string {
|
|
11
62
|
if (!VALID_TABLE_NAME.test(table)) {
|
|
12
63
|
throw new HistoryImportError(`Invalid history ETL target table: ${table}`, { table });
|
|
@@ -43,12 +94,12 @@ export async function applyHistoryImportSchema(db: ImportOptions['db']): Promise
|
|
|
43
94
|
* Ensure the ETL table(s) for a source definition exist.
|
|
44
95
|
*
|
|
45
96
|
* WHY: the static {@link HISTORY_IMPORT_SCHEMA_SQL} only creates the built-in
|
|
46
|
-
* `history_etl_*` tables. Custom source definitions
|
|
47
|
-
* with a `splitConfig.targetTable` override) need their
|
|
48
|
-
* on demand. The table name is already gated by
|
|
49
|
-
* {@link validateSourceDefinition} / {@link targetTableFor},
|
|
50
|
-
* interpolate into DDL. `CREATE TABLE IF NOT EXISTS` makes this
|
|
51
|
-
* built-in tables that the static schema already created.
|
|
97
|
+
* `history_etl_*` tables and the typed contract tables. Custom source definitions
|
|
98
|
+
* (and built-in definitions with a `splitConfig.targetTable` override) need their
|
|
99
|
+
* target table(s) created on demand. The table name is already gated by
|
|
100
|
+
* {@link VALID_TABLE_NAME} in {@link validateSourceDefinition} / {@link targetTableFor},
|
|
101
|
+
* so it is safe to interpolate into DDL. `CREATE TABLE IF NOT EXISTS` makes this
|
|
102
|
+
* idempotent for built-in tables that the static schema already created.
|
|
52
103
|
*/
|
|
53
104
|
async function ensureTargetTables(db: ImportOptions['db'], definition: SourceDefinition): Promise<void> {
|
|
54
105
|
const tables = new Set<string>([targetTableFor(definition.targetTable)]);
|
|
@@ -56,6 +107,8 @@ async function ensureTargetTables(db: ImportOptions['db'], definition: SourceDef
|
|
|
56
107
|
tables.add(targetTableFor(definition.splitConfig.targetTable));
|
|
57
108
|
}
|
|
58
109
|
for (const table of tables) {
|
|
110
|
+
// Typed tables are created by the static schema; use ETL DDL for custom tables.
|
|
111
|
+
if (TYPED_TABLE_COLUMNS[table] !== undefined) continue;
|
|
59
112
|
await db.exec(ETL_TABLE_DDL(table));
|
|
60
113
|
}
|
|
61
114
|
}
|
|
@@ -103,6 +156,8 @@ async function ledgerExists(db: ImportOptions['db'], recordHash: string): Promis
|
|
|
103
156
|
return row !== undefined && row !== null;
|
|
104
157
|
}
|
|
105
158
|
|
|
159
|
+
const _ensuredTables = new Set<string>();
|
|
160
|
+
|
|
106
161
|
async function insertRecord(
|
|
107
162
|
db: ImportOptions['db'],
|
|
108
163
|
targetTable: string,
|
|
@@ -114,17 +169,53 @@ async function insertRecord(
|
|
|
114
169
|
now: ImportOptions['now'],
|
|
115
170
|
): Promise<void> {
|
|
116
171
|
const table = targetTableFor(targetTable);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
172
|
+
const typedColumns = TYPED_TABLE_COLUMNS[table];
|
|
173
|
+
if (typedColumns !== undefined) {
|
|
174
|
+
// Typed insert path: map each column from the payload, assert no unknown keys.
|
|
175
|
+
const unknownKeys = Object.keys(payload).filter((k) => !typedColumns.includes(k) && !TYPED_IGNORED_KEYS.has(k));
|
|
176
|
+
if (unknownKeys.length > 0) {
|
|
177
|
+
throw new HistoryImportError(
|
|
178
|
+
`Typed table "${table}" has unknown columns: ${unknownKeys.join(', ')}. ` +
|
|
179
|
+
`Expected one of: ${typedColumns.join(', ')}.`,
|
|
180
|
+
{ table, unknownKeys, knownColumns: typedColumns },
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
// record_hash and imported_at come from the function parameters, not the payload.
|
|
184
|
+
const ts = timestamp(now);
|
|
185
|
+
const values = typedColumns.map((col) => {
|
|
186
|
+
if (col === 'record_hash') return recordHash;
|
|
187
|
+
if (col === 'imported_at') return ts;
|
|
188
|
+
const v = payload[col];
|
|
189
|
+
return v === undefined ? null : v;
|
|
190
|
+
});
|
|
191
|
+
if (!_ensuredTables.has(table)) {
|
|
192
|
+
// Typed tables are created by the static schema in applyHistoryImportSchema.
|
|
193
|
+
_ensuredTables.add(table);
|
|
194
|
+
}
|
|
195
|
+
await db.run(
|
|
196
|
+
`INSERT INTO ${table} (${typedColumns.join(', ')})
|
|
197
|
+
VALUES (${typedColumns.map(() => '?').join(', ')})
|
|
198
|
+
ON CONFLICT(record_hash) DO NOTHING`,
|
|
199
|
+
...values,
|
|
200
|
+
);
|
|
201
|
+
} else {
|
|
202
|
+
// Generic ETL insert path: store as payload_json blob.
|
|
203
|
+
if (!_ensuredTables.has(table)) {
|
|
204
|
+
await db.exec(ETL_TABLE_DDL(table));
|
|
205
|
+
_ensuredTables.add(table);
|
|
206
|
+
}
|
|
207
|
+
await db.run(
|
|
208
|
+
`INSERT INTO ${table} (record_hash, source_file, source_line, split_index, payload_json, imported_at)
|
|
209
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
210
|
+
ON CONFLICT(record_hash) DO NOTHING`,
|
|
211
|
+
recordHash,
|
|
212
|
+
sourceFile,
|
|
213
|
+
sourceLine,
|
|
214
|
+
splitIndex,
|
|
215
|
+
JSON.stringify(payload),
|
|
216
|
+
timestamp(now),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
128
219
|
}
|
|
129
220
|
|
|
130
221
|
async function insertLedger(
|