update on timeline on portfolio view + some corrections
This commit is contained in:
216
server/nocodb.js
Normal file
216
server/nocodb.js
Normal file
@@ -0,0 +1,216 @@
|
||||
require('dotenv').config({ path: __dirname + '/.env' });
|
||||
|
||||
const NOCODB_URL = process.env.NOCODB_URL || 'http://localhost:8090';
|
||||
const NOCODB_TOKEN = process.env.NOCODB_TOKEN;
|
||||
const NOCODB_BASE_ID = process.env.NOCODB_BASE_ID;
|
||||
|
||||
class NocoDBError extends Error {
|
||||
constructor(message, status, details) {
|
||||
super(message);
|
||||
this.name = 'NocoDBError';
|
||||
this.status = status;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache: table name → table ID
|
||||
const tableIdCache = {};
|
||||
|
||||
async function resolveTableId(tableName) {
|
||||
if (tableIdCache[tableName]) return tableIdCache[tableName];
|
||||
|
||||
const res = await fetch(`${NOCODB_URL}/api/v2/meta/bases/${NOCODB_BASE_ID}/tables`, {
|
||||
headers: { 'xc-token': NOCODB_TOKEN },
|
||||
});
|
||||
if (!res.ok) throw new NocoDBError('Failed to fetch tables', res.status);
|
||||
const data = await res.json();
|
||||
for (const t of data.list || []) {
|
||||
tableIdCache[t.title] = t.id;
|
||||
}
|
||||
if (!tableIdCache[tableName]) {
|
||||
throw new NocoDBError(`Table "${tableName}" not found in base ${NOCODB_BASE_ID}`, 404);
|
||||
}
|
||||
return tableIdCache[tableName];
|
||||
}
|
||||
|
||||
function buildWhere(conditions) {
|
||||
if (!conditions || conditions.length === 0) return '';
|
||||
return conditions
|
||||
.map(c => `(${c.field},${c.op},${c.value})`)
|
||||
.join('~and');
|
||||
}
|
||||
|
||||
async function request(method, url, body) {
|
||||
const opts = {
|
||||
method,
|
||||
headers: {
|
||||
'xc-token': NOCODB_TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
if (body !== undefined) opts.body = JSON.stringify(body);
|
||||
|
||||
const res = await fetch(url, opts);
|
||||
if (!res.ok) {
|
||||
let details;
|
||||
try { details = await res.json(); } catch {}
|
||||
throw new NocoDBError(
|
||||
`NocoDB ${method} ${url} failed: ${res.status}`,
|
||||
res.status,
|
||||
details
|
||||
);
|
||||
}
|
||||
// DELETE returns empty or {msg}
|
||||
const text = await res.text();
|
||||
return text ? JSON.parse(text) : {};
|
||||
}
|
||||
|
||||
// ─── Link Resolution ─────────────────────────────────────────
|
||||
|
||||
// Cache: "Table.Field" → { colId, tableId }
|
||||
const linkColCache = {};
|
||||
|
||||
async function getLinkColId(table, linkField) {
|
||||
const key = `${table}.${linkField}`;
|
||||
if (linkColCache[key]) return linkColCache[key];
|
||||
const tableId = await resolveTableId(table);
|
||||
const res = await fetch(`${NOCODB_URL}/api/v2/meta/tables/${tableId}`, {
|
||||
headers: { 'xc-token': NOCODB_TOKEN },
|
||||
});
|
||||
if (!res.ok) throw new NocoDBError('Failed to fetch table metadata', res.status);
|
||||
const meta = await res.json();
|
||||
for (const c of meta.columns || []) {
|
||||
if (c.uidt === 'Links' || c.uidt === 'LinkToAnotherRecord') {
|
||||
linkColCache[`${table}.${c.title}`] = { colId: c.id, tableId };
|
||||
}
|
||||
}
|
||||
return linkColCache[key] || null;
|
||||
}
|
||||
|
||||
async function fetchLinkedRecords(table, recordId, linkField) {
|
||||
const info = await getLinkColId(table, linkField);
|
||||
if (!info) return [];
|
||||
try {
|
||||
const data = await request('GET',
|
||||
`${NOCODB_URL}/api/v2/tables/${info.tableId}/links/${info.colId}/records/${recordId}`);
|
||||
return data.list || [];
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
async function resolveLinks(table, records, linkFields) {
|
||||
if (!records || !linkFields || linkFields.length === 0) return;
|
||||
const arr = Array.isArray(records) ? records : [records];
|
||||
const promises = [];
|
||||
for (const record of arr) {
|
||||
for (const field of linkFields) {
|
||||
const val = record[field];
|
||||
if (typeof val === 'number' && val > 0) {
|
||||
promises.push(
|
||||
fetchLinkedRecords(table, record.Id, field)
|
||||
.then(linked => { record[field] = linked; })
|
||||
);
|
||||
} else if (typeof val === 'number') {
|
||||
record[field] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
const nocodb = {
|
||||
/**
|
||||
* List records with optional filtering, sorting, pagination.
|
||||
* Pass `links: ['Field1','Field2']` to resolve linked records.
|
||||
*/
|
||||
async list(table, { where, sort, fields, limit, offset, links } = {}) {
|
||||
const tableId = await resolveTableId(table);
|
||||
const params = new URLSearchParams();
|
||||
if (where) params.set('where', typeof where === 'string' ? where : buildWhere(where));
|
||||
if (sort) params.set('sort', sort);
|
||||
if (fields) params.set('fields', Array.isArray(fields) ? fields.join(',') : fields);
|
||||
if (limit) params.set('limit', String(limit));
|
||||
if (offset) params.set('offset', String(offset));
|
||||
const qs = params.toString();
|
||||
const data = await request('GET', `${NOCODB_URL}/api/v2/tables/${tableId}/records${qs ? '?' + qs : ''}`);
|
||||
const records = data.list || [];
|
||||
if (links && links.length > 0) {
|
||||
await resolveLinks(table, records, links);
|
||||
}
|
||||
return records;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a single record by row ID.
|
||||
* Pass `{ links: ['Field1'] }` as third arg to resolve linked records.
|
||||
*/
|
||||
async get(table, rowId, { links } = {}) {
|
||||
const tableId = await resolveTableId(table);
|
||||
const record = await request('GET', `${NOCODB_URL}/api/v2/tables/${tableId}/records/${rowId}`);
|
||||
if (links && links.length > 0) {
|
||||
await resolveLinks(table, [record], links);
|
||||
}
|
||||
return record;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a single record, returns the created record
|
||||
*/
|
||||
async create(table, data) {
|
||||
const tableId = await resolveTableId(table);
|
||||
return request('POST', `${NOCODB_URL}/api/v2/tables/${tableId}/records`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a single record by row ID
|
||||
*/
|
||||
async update(table, rowId, data) {
|
||||
const tableId = await resolveTableId(table);
|
||||
return request('PATCH', `${NOCODB_URL}/api/v2/tables/${tableId}/records`, { Id: rowId, ...data });
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a single record by row ID
|
||||
*/
|
||||
async delete(table, rowId) {
|
||||
const tableId = await resolveTableId(table);
|
||||
return request('DELETE', `${NOCODB_URL}/api/v2/tables/${tableId}/records`, { Id: rowId });
|
||||
},
|
||||
|
||||
/**
|
||||
* Bulk create records
|
||||
*/
|
||||
async bulkCreate(table, records) {
|
||||
const tableId = await resolveTableId(table);
|
||||
return request('POST', `${NOCODB_URL}/api/v2/tables/${tableId}/records`, records);
|
||||
},
|
||||
|
||||
/**
|
||||
* Bulk update records (each must include Id)
|
||||
*/
|
||||
async bulkUpdate(table, records) {
|
||||
const tableId = await resolveTableId(table);
|
||||
return request('PATCH', `${NOCODB_URL}/api/v2/tables/${tableId}/records`, records);
|
||||
},
|
||||
|
||||
/**
|
||||
* Bulk delete records (each must include Id)
|
||||
*/
|
||||
async bulkDelete(table, records) {
|
||||
const tableId = await resolveTableId(table);
|
||||
return request('DELETE', `${NOCODB_URL}/api/v2/tables/${tableId}/records`, records);
|
||||
},
|
||||
|
||||
// Expose helpers
|
||||
buildWhere,
|
||||
resolveTableId,
|
||||
getLinkColId,
|
||||
NocoDBError,
|
||||
clearCache() { Object.keys(tableIdCache).forEach(k => delete tableIdCache[k]); },
|
||||
|
||||
// Config getters
|
||||
get url() { return NOCODB_URL; },
|
||||
get token() { return NOCODB_TOKEN; },
|
||||
get baseId() { return NOCODB_BASE_ID; },
|
||||
};
|
||||
|
||||
module.exports = nocodb;
|
||||
Reference in New Issue
Block a user