Skip to main content

Delete Operations (DELETE)

Overview

DELETE operations remove records through two modes: soft delete (default, reversible) and force delete (permanent, irreversible). CoreService routes DELETE requests to CoreWrite for validation and execution.

Endpoint:

DELETE /api/v4/core/{dim}/{dim_id}
DELETE /api/v4/core/{dim}/{dim_id}?force=true
Authorization: Bearer {token}

Two Deletion Modes:

  1. Soft Delete (default): Sets TREC='C', record remains in database
  2. Force Delete: Physically removes record from database

Soft Delete (Default)

Behavior

Default DELETE is soft (reversible):

Request:

DELETE /api/v4/core/PRD/123
Authorization: Bearer eyJhbGc...

Database Action:

-- Soft delete: UPDATE, not DELETE
UPDATE TB_ANAG_PRD00
SET TREC = 'C',
LDATA = '20251219160000',
LOWNER = 'admin@example.com'
WHERE PRD_ID = 123;

Response:

{
"status": "success",
"message": "Record soft deleted",
"data": {
"PRD_ID": 123,
"XPRD01": "Widget Pro",
"TREC": "C", // Changed to 'C' (Cancelled)
"LDATA": "20251219160000", // Last modification timestamp
"LOWNER": "admin@example.com" // Who deleted it
}
}

Benefits:

  • ✅ Reversible (can restore)
  • ✅ Maintains referential integrity
  • ✅ Preserves audit trail
  • ✅ "Trash" functionality possible

TREC State Transition

TREC='N' or TREC='M'
↓ DELETE (soft)
TREC='C' (Cancelled)

Audit Fields:

  • TREC: Changed to 'C'
  • LDATA: Set to deletion timestamp
  • LOWNER: Set to user who deleted
  • CDATA/OWNER: Preserved (original creation info)

Query Behavior After Soft Delete

Default queries exclude TREC='C':

# This won't return soft-deleted products
GET /api/v4/core/PRD?source=productList

To include soft-deleted records:

# Show deleted records
GET /api/v4/core/PRD?source=productList&cond_trec=C

# Show all records (including deleted)
GET /api/v4/core/PRD?source=productList&cond_trec=N,M,C

Force Delete (Permanent)

Behavior

Force delete physically removes record:

Request:

DELETE /api/v4/core/PRD/123?force=true
Authorization: Bearer eyJhbGc...

Database Action:

-- Force delete: Physical DELETE
DELETE FROM TB_ANAG_PRD00
WHERE PRD_ID = 123;

Response:

{
"status": "success",
"message": "Record permanently deleted"
}

⚠️ Warning: Force delete is irreversible. Use with extreme caution.

When to Use Force Delete

Rarely needed:

  • GDPR "right to be forgotten" compliance
  • Test data cleanup
  • Removing invalid/corrupted records
  • Database maintenance

Usually avoid:

  • Regular deletion (use soft delete instead)
  • User-initiated deletes (offer restore functionality)
  • Production data (maintain audit trail)

Restore (Undelete)

Restore Soft-Deleted Record

Soft-deleted records can be restored via PATCH:

# Restore by setting TREC='M'
PATCH /api/v4/core/PRD/123
{
"data": {
"TREC": "M" // Or "N" depending on use case
}
}

Result:

UPDATE TB_ANAG_PRD00
SET TREC = 'M',
LDATA = '20251219170000',
LOWNER = 'admin@example.com'
WHERE PRD_ID = 123 AND TREC = 'C';

DELETE_CASCADE Operations

Soft Delete Cascades

TB_COST.DELETE_CASCADE triggers related deletions:

-- When order is soft-deleted, soft-delete all items
INSERT INTO TB_COST (COD_DIM, NUM_COST, COD_VAR, DELETE_CASCADE) VALUES
('ORD', 10, 'items', 'ORDITEM:XORDITEM_ORDER_ID:soft');

Single Request Cascades to Related Records:

Request:

DELETE /api/v4/core/ORD/123

Result:

-- Soft delete order
UPDATE TB_ANAG_ORD00
SET TREC = 'C', LDATA = '20251219160000'
WHERE ORD_ID = 123;

-- Cascade: Soft delete all order items
UPDATE TB_ANAG_ORDITEM00
SET TREC = 'C', LDATA = '20251219160000'
WHERE XORDITEM_ORDER_ID = 123;

Force Delete Cascades

Force delete can cascade permanently:

-- When order is force-deleted, force-delete all items
INSERT INTO TB_COST (COD_DIM, NUM_COST, COD_VAR, DELETE_CASCADE) VALUES
('ORD', 10, 'items', 'ORDITEM:XORDITEM_ORDER_ID:force');

Request:

DELETE /api/v4/core/ORD/123?force=true

Result:

-- Force delete order
DELETE FROM TB_ANAG_ORD00
WHERE ORD_ID = 123;

-- Cascade: Force delete all order items
DELETE FROM TB_ANAG_ORDITEM00
WHERE XORDITEM_ORDER_ID = 123;

⚠️ Danger: Force cascades are irreversible for all related records.

Outbox Event

Delete Event Publication

Every DELETE publishes event to RabbitMQ:

Soft Delete Event:

{
"event_id": "evt_770g0622",
"event_type": "ProductDeleted",
"aggregate_id": "123",
"aggregate_type": "PRD",
"operation": "DELETE",
"deletion_type": "soft",
"timestamp": "2025-12-19T16:00:00Z",
"payload": {
"PRD_ID": 123,
"XPRD01": "Widget Pro",
"TREC": "C"
},
"user": "admin@example.com"
}

Force Delete Event:

{
"event_id": "evt_880h1733",
"event_type": "ProductForceDeleted",
"aggregate_id": "123",
"aggregate_type": "PRD",
"operation": "DELETE",
"deletion_type": "force",
"timestamp": "2025-12-19T17:00:00Z",
"payload": {
"PRD_ID": 123,
"XPRD01": "Widget Pro"
},
"user": "admin@example.com"
}

JavaScript Implementation

Delete Service

class ProductService {
constructor(apiBase, token) {
this.apiBase = apiBase;
this.token = token;
}

/**
* Soft delete (default)
*/
async deleteProduct(productId) {
const response = await fetch(`${this.apiBase}/api/v4/core/PRD/${productId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${this.token}` }
});

if (!response.ok) {
const error = await response.json();
throw new ProductDeleteError(error);
}

return response.json();
}

/**
* Force delete (permanent)
*/
async forceDeleteProduct(productId) {
const url = new URL(`${this.apiBase}/api/v4/core/PRD/${productId}`);
url.searchParams.append('force', 'true');

const response = await fetch(url, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${this.token}` }
});

if (!response.ok) {
const error = await response.json();
throw new ProductDeleteError(error);
}

return response.json();
}

/**
* Restore soft-deleted product
*/
async restoreProduct(productId) {
const response = await fetch(`${this.apiBase}/api/v4/core/PRD/${productId}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
data: { TREC: 'M' }
})
});

if (!response.ok) {
const error = await response.json();
throw new ProductRestoreError(error);
}

return response.json();
}

/**
* Check if product is soft-deleted
*/
async isDeleted(productId) {
const url = new URL(`${this.apiBase}/api/v4/core/PRD/${productId}`);
url.searchParams.append('cond_trec', 'N,M,C'); // Include deleted

const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${this.token}` }
});

const result = await response.json();
return result.data?.TREC === 'C';
}
}

class ProductDeleteError extends Error {
constructor(errorResponse) {
super(errorResponse.message);
this.code = errorResponse.code;
}
}

class ProductRestoreError extends Error {
constructor(errorResponse) {
super(errorResponse.message);
this.code = errorResponse.code;
}
}

// Usage
const productService = new ProductService(apiBase, token);

// Soft delete
await productService.deleteProduct(123);

// Restore
await productService.restoreProduct(123);

// Force delete (with confirmation)
if (confirm('Permanently delete? This cannot be undone!')) {
await productService.forceDeleteProduct(123);
}

Trash Management

class TrashService {
constructor(apiBase, token, dimension) {
this.apiBase = apiBase;
this.token = token;
this.dimension = dimension;
}

/**
* Get trash (soft-deleted records)
*/
async getTrash(options = {}) {
const url = new URL(`${this.apiBase}/api/v4/core/${this.dimension}`);
url.searchParams.append('source', options.source || 'list');
url.searchParams.append('cond_trec', 'C'); // Only soft-deleted
url.searchParams.append('$order', 'LDATA DESC'); // Recently deleted first
url.searchParams.append('$num_rows', options.limit || 25);
url.searchParams.append('$offset', options.offset || 0);

const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${this.token}` }
});

return response.json();
}

/**
* Empty trash (force delete all soft-deleted records)
*/
async emptyTrash() {
// 1. Get all soft-deleted records
const trash = await this.getTrash({ limit: 1000 });

// 2. Force delete each
const deletions = trash.data.map(record =>
fetch(`${this.apiBase}/api/v4/core/${this.dimension}/${record[`${this.dimension}_ID`]}?force=true`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${this.token}` }
})
);

return Promise.all(deletions);
}

/**
* Restore from trash
*/
async restore(recordId) {
const response = await fetch(`${this.apiBase}/api/v4/core/${this.dimension}/${recordId}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
data: { TREC: 'M' }
})
});

return response.json();
}
}

// Usage
const trash = new TrashService(apiBase, token, 'PRD');

// Get trash
const deleted = await trash.getTrash();
console.log(`${deleted.data.length} items in trash`);

// Restore item
await trash.restore(123);

// Empty trash (with confirmation)
if (confirm('Permanently delete all items in trash?')) {
await trash.emptyTrash();
}

Common Patterns

Pattern 1: Soft Delete with Confirmation

async function deleteProductWithConfirmation(productId) {
if (!confirm('Move product to trash?')) {
return;
}

try {
await fetch(`${apiBase}/api/v4/core/PRD/${productId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});

alert('Product moved to trash. You can restore it later.');
} catch (error) {
alert('Failed to delete product');
}
}

Pattern 2: Trash View with Restore

async function loadTrash() {
const url = new URL(`${apiBase}/api/v4/core/PRD`);
url.searchParams.append('source', 'productList');
url.searchParams.append('cond_trec', 'C'); // Only deleted
url.searchParams.append('$order', 'LDATA DESC');

const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${token}` }
});

const trash = await response.json();

// Render trash items with restore buttons
trash.data.forEach(product => {
renderTrashItem(product, async () => {
// Restore callback
await fetch(`${apiBase}/api/v4/core/PRD/${product.PRD_ID}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ data: { TREC: 'M' } })
});

alert('Product restored!');
loadTrash(); // Refresh
});
});
}

Pattern 3: Auto-Cleanup (Force Delete Old Soft-Deleted Records)

async function autoCleanup(daysOld = 30) {
// Calculate cutoff date
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
const cutoffTimestamp = cutoffDate.toISOString().replace(/[-:T.]/g, '').slice(0, 14);

// Get old soft-deleted records
const url = new URL(`${apiBase}/api/v4/core/PRD`);
url.searchParams.append('source', 'productList');
url.searchParams.append('cond_trec', 'C'); // Only soft-deleted
url.searchParams.append('$filter', `LDATA lt '${cutoffTimestamp}'`);

const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${token}` }
});

const oldDeleted = await response.json();

// Force delete each
const deletions = oldDeleted.data.map(product =>
fetch(`${apiBase}/api/v4/core/PRD/${product.PRD_ID}?force=true`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
})
);

await Promise.all(deletions);

console.log(`Auto-cleaned ${oldDeleted.data.length} records older than ${daysOld} days`);
}

// Run daily cleanup
setInterval(() => autoCleanup(30), 24 * 60 * 60 * 1000);

Best Practices

✅ DO:

Use soft delete by default:

// ✅ Good - reversible
DELETE /api/v4/core/PRD/123

Confirm before force delete:

// ✅ Good
if (confirm('Permanently delete? This cannot be undone!')) {
DELETE /api/v4/core/PRD/123?force=true
}

Provide restore functionality:

// ✅ Good - users can restore
async function restoreProduct(id) {
await fetch(`${apiBase}/api/v4/core/PRD/${id}`, {
method: 'PATCH',
body: JSON.stringify({ data: { TREC: 'M' } })
});
}

Auto-cleanup old soft-deleted records:

// ✅ Good - force delete after 30 days
await autoCleanup(30);

❌ DON'T:

Don't use force delete for regular deletions:

// ❌ Bad - irreversible by default
DELETE /api/v4/core/PRD/123?force=true

// ✅ Good - soft delete first
DELETE /api/v4/core/PRD/123

Don't forget to handle cascades:

// ❌ Bad - orphaned order items
DELETE /api/v4/core/ORD/123
// Order items left without parent

// ✅ Good - configure DELETE_CASCADE in metadata
// TB_COST.DELETE_CASCADE handles related records

Don't force delete without confirmation:

// ❌ Bad - no warning
await forceDeleteProduct(123);

// ✅ Good - confirm first
if (confirm('Permanently delete?')) {
await forceDeleteProduct(123);
}

Summary

  • ✅ Soft delete (default): Sets TREC='C', reversible
  • ✅ Force delete (?force=true): Physical removal, irreversible
  • ✅ Soft-deleted records excluded from default queries
  • ✅ Restore via PATCH setting TREC='M'
  • ✅ DELETE_CASCADE propagates deletions to related records
  • ✅ Outbox event published to RabbitMQ
  • ✅ Use cond_trec=C to query soft-deleted records

Key Takeaways:

  1. Default DELETE is soft (TREC='C'), not physical removal
  2. Force delete (?force=true) is permanent and irreversible
  3. Soft delete enables "trash" and restore functionality
  4. Always confirm before force delete
  5. DELETE_CASCADE handles related record deletion
  6. Auto-cleanup old soft-deleted records (e.g., after 30 days)

Decision Matrix:

Use CaseOperation
User deletes recordSoft delete (TREC='C')
Restore from trashPATCH with TREC='M'
GDPR data removalForce delete (?force=true)
Test data cleanupForce delete (?force=true)
30+ day old deleted recordsForce delete (auto-cleanup)

Next: Validation →