update on timeline on portfolio view + some corrections

This commit is contained in:
fahed
2026-02-10 13:20:49 +03:00
parent d15e54044e
commit 334727b232
37 changed files with 5119 additions and 1440 deletions

216
server/nocodb.js Normal file
View 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;