- Add import_id column to records (links each record to its import batch) - import_records() now stores readable dedup field values (not hashes) in info.inserted_keys / info.excluded_keys, and stamps import_id on insert - delete_import() simplified to delete log row; ON DELETE CASCADE removes records - Add get_import_log() and get_all_import_logs() DB functions - Add DELETE /api/sources/:name/import-log/:id endpoint - Add GET /api/sources/import-log global log endpoint - Import route now auto-applies transformations to new records after import - Import page: show ID column, expandable key detail, checkbox delete - New Log page: global view of all imports across sources - Update README API reference and workflow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
103 lines
4.3 KiB
JavaScript
103 lines
4.3 KiB
JavaScript
const BASE = '/api'
|
|
|
|
let _credentials = null // { user, pass }
|
|
|
|
export function setCredentials(user, pass) {
|
|
_credentials = { user, pass }
|
|
}
|
|
|
|
export function clearCredentials() {
|
|
_credentials = null
|
|
}
|
|
|
|
async function request(method, path, body, isFormData = false) {
|
|
const opts = { method, headers: {} }
|
|
|
|
if (_credentials) {
|
|
opts.headers['Authorization'] = `Basic ${btoa(`${_credentials.user}:${_credentials.pass}`)}`
|
|
}
|
|
|
|
if (body) {
|
|
if (isFormData) {
|
|
opts.body = body
|
|
} else {
|
|
opts.headers['Content-Type'] = 'application/json'
|
|
opts.body = JSON.stringify(body)
|
|
}
|
|
}
|
|
|
|
const res = await fetch(BASE + path, opts)
|
|
|
|
if (res.status === 401) {
|
|
clearCredentials()
|
|
const err = new Error('Unauthorized')
|
|
err.status = 401
|
|
throw err
|
|
}
|
|
|
|
const data = await res.json()
|
|
if (!res.ok) throw new Error(data.error || 'Request failed')
|
|
return data
|
|
}
|
|
|
|
export const api = {
|
|
// Sources
|
|
getSources: () => request('GET', '/sources'),
|
|
getSource: (name) => request('GET', `/sources/${name}`),
|
|
createSource: (body) => request('POST', '/sources', body),
|
|
updateSource: (name, body) => request('PUT', `/sources/${name}`, body),
|
|
deleteSource: (name) => request('DELETE', `/sources/${name}`),
|
|
suggestSource: (file) => {
|
|
const fd = new FormData()
|
|
fd.append('file', file)
|
|
return request('POST', '/sources/suggest', fd, true)
|
|
},
|
|
getImportLog: (name) => request('GET', `/sources/${name}/import-log`),
|
|
getAllImportLog: () => request('GET', '/sources/import-log'),
|
|
deleteImport: (name, id) => request('DELETE', `/sources/${name}/import-log/${id}`),
|
|
getStats: (name) => request('GET', `/sources/${name}/stats`),
|
|
importCSV: (name, file) => {
|
|
const fd = new FormData()
|
|
fd.append('file', file)
|
|
return request('POST', `/sources/${name}/import`, fd, true)
|
|
},
|
|
transform: (name) => request('POST', `/sources/${name}/transform`),
|
|
reprocess: (name) => request('POST', `/sources/${name}/reprocess`),
|
|
generateView: (name) => request('POST', `/sources/${name}/view`),
|
|
getFields: (name) => request('GET', `/sources/${name}/fields`),
|
|
getViewData: (name, limit = 100, offset = 0, sortCol = null, sortDir = 'asc') => {
|
|
const params = new URLSearchParams({ limit, offset })
|
|
if (sortCol) { params.set('sort_col', sortCol); params.set('sort_dir', sortDir) }
|
|
return request('GET', `/sources/${name}/view-data?${params}`)
|
|
},
|
|
|
|
// Rules
|
|
getRules: (source) => request('GET', `/rules/source/${source}`),
|
|
createRule: (body) => request('POST', '/rules', body),
|
|
updateRule: (id, body) => request('PUT', `/rules/${id}`, body),
|
|
deleteRule: (id) => request('DELETE', `/rules/${id}`),
|
|
testRule: (id, limit = 20) => request('GET', `/rules/${id}/test?limit=${limit}`),
|
|
previewRule: (source, field, pattern, flags, function_type = 'extract', replace_value = '', limit = 20) =>
|
|
request('GET', `/rules/preview?source=${encodeURIComponent(source)}&field=${encodeURIComponent(field)}&pattern=${encodeURIComponent(pattern)}&flags=${encodeURIComponent(flags || '')}&function_type=${function_type}&replace_value=${encodeURIComponent(replace_value)}&limit=${limit}`),
|
|
|
|
// Mappings
|
|
getMappings: (source, rule) => request('GET', `/mappings/source/${source}${rule ? `?rule_name=${rule}` : ''}`),
|
|
getMappingCounts: (source, rule) => request('GET', `/mappings/source/${source}/counts${rule ? `?rule_name=${rule}` : ''}`),
|
|
getUnmapped: (source, rule) => request('GET', `/mappings/source/${source}/unmapped${rule ? `?rule_name=${rule}` : ''}`),
|
|
getAllValues: (source, rule) => request('GET', `/mappings/source/${source}/all-values${rule ? `?rule_name=${rule}` : ''}`),
|
|
exportMappingsUrl: (source, rule) => `${BASE}/mappings/source/${source}/export.tsv${rule ? `?rule_name=${rule}` : ''}`,
|
|
importMappingsCSV: (source, file) => {
|
|
const fd = new FormData()
|
|
fd.append('file', file)
|
|
return request('POST', `/mappings/source/${source}/import-csv`, fd, true)
|
|
},
|
|
createMapping: (body) => request('POST', '/mappings', body),
|
|
bulkMappings: (mappings) => request('POST', '/mappings/bulk', { mappings }),
|
|
updateMapping: (id, body) => request('PUT', `/mappings/${id}`, body),
|
|
deleteMapping: (id) => request('DELETE', `/mappings/${id}`),
|
|
|
|
// Records
|
|
getRecords: (source, limit = 100, offset = 0) =>
|
|
request('GET', `/records/source/${source}?limit=${limit}&offset=${offset}`),
|
|
}
|