feat: artefact version auto-advance, post_id on artefacts, test-email endpoint
Deploy / deploy (push) Successful in 13s
Deploy / deploy (push) Successful in 13s
- Auto-advance artefact to next working version on rejection/revision - Add post_id field to artefact creation - Add request timeout (20s) to NocoDB client - Add POST /api/admin/test-email for diagnosing SMTP issues - Fix FK column creation logging Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+103
-5
@@ -591,12 +591,17 @@ async function ensureFKColumns() {
|
||||
|
||||
for (const col of columns) {
|
||||
if (!existingCols.has(col)) {
|
||||
console.log(` Adding column ${table}.${col}...`);
|
||||
await fetch(`${nocodb.url}/api/v2/meta/tables/${tableId}/columns`, {
|
||||
console.log(` Adding FK column ${table}.${col}...`);
|
||||
const colRes = await fetch(`${nocodb.url}/api/v2/meta/tables/${tableId}/columns`, {
|
||||
method: 'POST',
|
||||
headers: { 'xc-token': nocodb.token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: col, uidt: 'Number' }),
|
||||
});
|
||||
if (colRes.ok) {
|
||||
console.log(` ✓ Created ${table}.${col}`);
|
||||
} else {
|
||||
console.warn(` ⚠ Failed to create ${table}.${col}: ${colRes.status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -668,6 +673,28 @@ app.get('/api/health', async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── EMAIL TEST ─────────────────────────────────────────────────
|
||||
|
||||
app.post('/api/admin/test-email', requireAuth, async (req, res) => {
|
||||
if (req.session.userRole !== 'superadmin') return res.status(403).json({ error: 'Superadmin only' });
|
||||
const to = req.body.to || req.session.userEmail;
|
||||
if (!to) return res.status(400).json({ error: 'No recipient — pass { "to": "email@example.com" }' });
|
||||
const { sendMail, getSmtpConfig } = require('./mail');
|
||||
const config = getSmtpConfig();
|
||||
if (!config) return res.status(503).json({ error: 'SMTP not configured', env: { server: !!process.env.CLOUDRON_MAIL_SMTP_SERVER || !!process.env.MAIL_SMTP_SERVER } });
|
||||
try {
|
||||
const info = await sendMail({
|
||||
to,
|
||||
subject: 'Rawaj — Test Email',
|
||||
html: '<p>If you received this, email delivery is working correctly.</p>',
|
||||
text: 'If you received this, email delivery is working correctly.',
|
||||
});
|
||||
res.json({ success: true, to, messageId: info?.messageId, smtp: { host: config.host, port: config.port, from: config.from } });
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, error: err.message, code: err.code, smtp: { host: config.host, port: config.port } });
|
||||
}
|
||||
});
|
||||
|
||||
// ─── SETUP ROUTES ───────────────────────────────────────────────
|
||||
|
||||
app.get('/api/setup/status', async (req, res) => {
|
||||
@@ -4007,7 +4034,7 @@ app.get('/api/artefacts/:id', requireAuth, async (req, res) => {
|
||||
});
|
||||
|
||||
app.post('/api/artefacts', requireAuth, async (req, res) => {
|
||||
const { title, description, type, brand_id, content, project_id, campaign_id, approver_ids, copy_type } = req.body;
|
||||
const { title, description, type, brand_id, content, project_id, campaign_id, approver_ids, copy_type, post_id } = req.body;
|
||||
if (!title) return res.status(400).json({ error: 'Title is required' });
|
||||
|
||||
try {
|
||||
@@ -4018,6 +4045,7 @@ app.post('/api/artefacts', requireAuth, async (req, res) => {
|
||||
status: 'draft',
|
||||
content: content || null,
|
||||
brand_id: brand_id ? Number(brand_id) : null,
|
||||
post_id: post_id ? Number(post_id) : null,
|
||||
project_id: project_id ? Number(project_id) : null,
|
||||
campaign_id: campaign_id ? Number(campaign_id) : null,
|
||||
approver_ids: approver_ids || null,
|
||||
@@ -4025,7 +4053,7 @@ app.post('/api/artefacts', requireAuth, async (req, res) => {
|
||||
created_by_user_id: req.session.userId,
|
||||
current_version: 1,
|
||||
};
|
||||
console.log('[POST /artefacts] Creating with:', JSON.stringify({ approver_ids: createData.approver_ids, project_id: createData.project_id, campaign_id: createData.campaign_id }));
|
||||
console.log('[POST /artefacts] Creating with:', JSON.stringify({ post_id: createData.post_id, type: createData.type, copy_type: createData.copy_type, approver_ids: createData.approver_ids }));
|
||||
const created = await nocodb.create('Artefacts', createData);
|
||||
console.log('[POST /artefacts] NocoDB returned:', JSON.stringify({ Id: created.Id, approver_ids: created.approver_ids }));
|
||||
|
||||
@@ -4039,7 +4067,7 @@ app.post('/api/artefacts', requireAuth, async (req, res) => {
|
||||
});
|
||||
|
||||
const artefact = await nocodb.get('Artefacts', created.Id);
|
||||
console.log('[POST /artefacts] After re-read:', JSON.stringify({ Id: artefact.Id, approver_ids: artefact.approver_ids }));
|
||||
console.log('[POST /artefacts] After re-read:', JSON.stringify({ Id: artefact.Id, post_id: artefact.post_id, copy_type: artefact.copy_type, approver_ids: artefact.approver_ids }));
|
||||
const approverIdList = artefact.approver_ids ? artefact.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [];
|
||||
const approvers = [];
|
||||
for (const id of approverIdList) {
|
||||
@@ -4267,6 +4295,45 @@ app.post('/api/artefacts/:id/link-post', requireAuth, async (req, res) => {
|
||||
|
||||
// ─── ARTEFACT VERSIONS ──────────────────────────────────────────
|
||||
|
||||
// Creates the next working version automatically after rejection/revision.
|
||||
// For copy artefacts, texts are copied from the last version so the creator
|
||||
// doesn't start with a blank slate.
|
||||
async function autoAdvanceArtefactVersion(artefact, nocodb, QUERY_LIMITS) {
|
||||
const versions = await nocodb.list('ArtefactVersions', {
|
||||
where: `(artefact_id,eq,${artefact.Id})`,
|
||||
sort: '-version_number',
|
||||
limit: 1,
|
||||
});
|
||||
const latest = versions[0];
|
||||
if (!latest) return;
|
||||
|
||||
const nextNumber = latest.version_number + 1;
|
||||
|
||||
const created = await nocodb.create('ArtefactVersions', {
|
||||
artefact_id: artefact.Id,
|
||||
version_number: nextNumber,
|
||||
created_at: new Date().toISOString(),
|
||||
notes: `Round ${nextNumber}`,
|
||||
});
|
||||
|
||||
if (artefact.type === 'copy') {
|
||||
const prevTexts = await nocodb.list('ArtefactVersionTexts', {
|
||||
where: `(version_id,eq,${latest.Id})`,
|
||||
limit: QUERY_LIMITS.large,
|
||||
});
|
||||
for (const text of prevTexts) {
|
||||
await nocodb.create('ArtefactVersionTexts', {
|
||||
version_id: created.Id,
|
||||
language_code: text.language_code,
|
||||
language_label: text.language_label,
|
||||
content: text.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await nocodb.update('Artefacts', artefact.Id, { current_version: nextNumber });
|
||||
}
|
||||
|
||||
// List all versions for an artefact
|
||||
app.get('/api/artefacts/:id/versions', requireAuth, async (req, res) => {
|
||||
try {
|
||||
@@ -4466,6 +4533,31 @@ app.post('/api/artefacts/:id/versions/:versionId/texts', requireAuth, async (req
|
||||
}
|
||||
});
|
||||
|
||||
// Update language entry content
|
||||
app.patch('/api/artefact-version-texts/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const text = await nocodb.get('ArtefactVersionTexts', req.params.id);
|
||||
if (!text) return res.status(404).json({ error: 'Text not found' });
|
||||
|
||||
const version = await nocodb.get('ArtefactVersions', text.version_id);
|
||||
const artefact = await nocodb.get('Artefacts', version.artefact_id);
|
||||
|
||||
if (req.session.userRole === 'contributor' && artefact.created_by_user_id !== req.session.userId) {
|
||||
return res.status(403).json({ error: 'You can only manage texts for your own artefacts' });
|
||||
}
|
||||
|
||||
const { content } = req.body;
|
||||
if (content === undefined) return res.status(400).json({ error: 'content is required' });
|
||||
|
||||
await nocodb.update('ArtefactVersionTexts', req.params.id, { content });
|
||||
const updated = await nocodb.get('ArtefactVersionTexts', req.params.id);
|
||||
res.json(updated);
|
||||
} catch (err) {
|
||||
console.error('Update text error:', err);
|
||||
res.status(500).json({ error: 'Failed to update text' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete language entry
|
||||
app.delete('/api/artefact-version-texts/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
@@ -4823,6 +4915,9 @@ app.post('/api/public/review/:token/reject', async (req, res) => {
|
||||
feedback: feedback || '',
|
||||
});
|
||||
|
||||
// Auto-advance to next working version
|
||||
await autoAdvanceArtefactVersion(artefact, nocodb, QUERY_LIMITS);
|
||||
|
||||
res.json({ success: true, message: 'Artefact rejected' });
|
||||
notify.notifyRejected({ type: 'artefact', record: artefact, approverName: approved_by_name, feedback });
|
||||
} catch (err) {
|
||||
@@ -4853,6 +4948,9 @@ app.post('/api/public/review/:token/revision', async (req, res) => {
|
||||
feedback: feedback || '',
|
||||
});
|
||||
|
||||
// Auto-advance to next working version
|
||||
await autoAdvanceArtefactVersion(artefact, nocodb, QUERY_LIMITS);
|
||||
|
||||
res.json({ success: true, message: 'Revision requested' });
|
||||
notify.notifyRevisionRequested({ record: artefact, approverName: approved_by_name, feedback });
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user