From 2b61be0c385801777a46707ee16e53c858466335 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Thu, 12 Feb 2026 10:49:07 +0200 Subject: [PATCH 1/9] Remove MAX_DELETE_PER_RUN --- index.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/index.ts b/index.ts index 07b0dee..0044af4 100644 --- a/index.ts +++ b/index.ts @@ -3,8 +3,6 @@ import type { IAdminForth, IHttpServer, AdminForthResource } from "adminforth"; import type { PluginOptions } from './types.js'; import { parseHumanNumber } from './utils/parseNumber.js'; import { parseDuration } from './utils/parseDuration.js'; -// Why do we need MAX_DELETE_PER_RUN? -const MAX_DELETE_PER_RUN = 500; export default class AutoRemovePlugin extends AdminForthPlugin { options: PluginOptions; @@ -72,7 +70,7 @@ export default class AutoRemovePlugin extends AdminForthPlugin { const allRecords = await resource.list([], null, null, [Sorts.ASC(this.options.createdAtField)]); if (allRecords.length <= limit) return; - const toDelete = allRecords.slice(0, allRecords.length - limit).slice(0, this.options.maxDeletePerRun || MAX_DELETE_PER_RUN); + const toDelete = allRecords.slice(0, allRecords.length - limit).slice(0, this.options.maxDeletePerRun); for (const r of toDelete) { await resource.delete(r[this._resourceConfig.columns.find(c => c.primaryKey)!.name]); console.log(`AutoRemovePlugin: deleted record ${r[this._resourceConfig.columns.find(c => c.primaryKey)!.name]} due to count-based limit`); @@ -87,7 +85,7 @@ export default class AutoRemovePlugin extends AdminForthPlugin { const allRecords = await resource.list([], null, null, Sorts.ASC(this.options.createdAtField)); const toDelete = allRecords .filter(r => new Date(r[this.options.createdAtField]).getTime() < threshold) - .slice(0, this.options.maxDeletePerRun || MAX_DELETE_PER_RUN); + .slice(0, this.options.maxDeletePerRun); for (const r of toDelete) { await resource.delete(r[this._resourceConfig.columns.find(c => c.primaryKey)!.name]); From bd1a2d165c0e8c89e99e7bf5920c2843bca7399b Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Thu, 12 Feb 2026 11:17:38 +0200 Subject: [PATCH 2/9] fix: change error message for created at field --- index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/index.ts b/index.ts index 0044af4..026434d 100644 --- a/index.ts +++ b/index.ts @@ -36,8 +36,7 @@ export default class AutoRemovePlugin extends AdminForthPlugin { validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) { // Check createdAtField exists and is date/datetim const col = resourceConfig.columns.find(c => c.name === this.options.createdAtField); - // I don't like error messages look at other plugins and change to something similar - if (!col) throw new Error(`createdAtField "${this.options.createdAtField}" not found`); + if (!col) throw new Error(`Field "${this.options.createdAtField}" not found in resource "${resourceConfig.label}"`); if (![AdminForthDataTypes.DATE, AdminForthDataTypes.DATETIME].includes(col.type!)) { throw new Error(`createdAtField must be date/datetime/timestamp`); } From 4a813211774fca271a999cba4b6befd66f97d95d Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Thu, 12 Feb 2026 11:24:32 +0200 Subject: [PATCH 3/9] Fix: change error message for date/datetime --- index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/index.ts b/index.ts index 026434d..4070401 100644 --- a/index.ts +++ b/index.ts @@ -34,11 +34,10 @@ export default class AutoRemovePlugin extends AdminForthPlugin { } validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) { - // Check createdAtField exists and is date/datetim const col = resourceConfig.columns.find(c => c.name === this.options.createdAtField); if (!col) throw new Error(`Field "${this.options.createdAtField}" not found in resource "${resourceConfig.label}"`); if (![AdminForthDataTypes.DATE, AdminForthDataTypes.DATETIME].includes(col.type!)) { - throw new Error(`createdAtField must be date/datetime/timestamp`); + throw new Error(`Field "${this.options.createdAtField}" in resource "${resourceConfig.label}" must be of type DATE or DATETIME`); } // Check mode-specific options From 0950aab1d562d994911fe3f68f1f656b99c4ece2 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Thu, 12 Feb 2026 14:38:46 +0200 Subject: [PATCH 4/9] Refactor: streamline resource handling in AutoRemovePlugin --- index.ts | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/index.ts b/index.ts index 4070401..db348b1 100644 --- a/index.ts +++ b/index.ts @@ -6,11 +6,8 @@ import { parseDuration } from './utils/parseDuration.js'; export default class AutoRemovePlugin extends AdminForthPlugin { options: PluginOptions; - // I don't understand why do you need this resource config if you alredy have it below - // You can use create resource: AdminForthResourc and somewhere below just set it - // Then you will remove [this._resourceConfig.columns.find(c => c.primaryKey)!.name] and will use just resource - protected _resourceConfig!: AdminForthResource; - private timer?: NodeJS.Timeout; + resource: AdminForthResource; + timer: NodeJS.Timeout; constructor(options: PluginOptions) { super(options, import.meta.url); @@ -24,9 +21,8 @@ export default class AutoRemovePlugin extends AdminForthPlugin { async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) { super.modifyResourceConfig(adminforth, resourceConfig); - this._resourceConfig = resourceConfig; + this.resource = resourceConfig; - // Start the cleanup timer const intervalMs = parseDuration(this.options.interval || '1d'); this.timer = setInterval(() => { this.runCleanup(adminforth).catch(console.error); @@ -63,31 +59,31 @@ export default class AutoRemovePlugin extends AdminForthPlugin { private async cleanupByCount(adminforth: IAdminForth) { const limit = parseHumanNumber(this.options.maxItems!); - const resource = adminforth.resource(this._resourceConfig.resourceId); + const resource = adminforth.resource(this.resource.resourceId); const allRecords = await resource.list([], null, null, [Sorts.ASC(this.options.createdAtField)]); if (allRecords.length <= limit) return; const toDelete = allRecords.slice(0, allRecords.length - limit).slice(0, this.options.maxDeletePerRun); for (const r of toDelete) { - await resource.delete(r[this._resourceConfig.columns.find(c => c.primaryKey)!.name]); - console.log(`AutoRemovePlugin: deleted record ${r[this._resourceConfig.columns.find(c => c.primaryKey)!.name]} due to count-based limit`); + await resource.delete(r[this.resource.columns.find(c => c.primaryKey)!.name]); + console.log(`AutoRemovePlugin: deleted record ${r[this.resource.columns.find(c => c.primaryKey)!.name]} due to count-based limit`); } } private async cleanupByTime(adminforth: IAdminForth) { const maxAgeMs = parseDuration(this.options.maxAge!); const threshold = Date.now() - maxAgeMs; - const resource = adminforth.resource(this._resourceConfig.resourceId); + const resource = adminforth.resource(this.resource.resourceId); - const allRecords = await resource.list([], null, null, Sorts.ASC(this.options.createdAtField)); + const allRecords = await resource.list([], null, null, [Sorts.ASC(this.options.createdAtField)]); const toDelete = allRecords .filter(r => new Date(r[this.options.createdAtField]).getTime() < threshold) .slice(0, this.options.maxDeletePerRun); for (const r of toDelete) { - await resource.delete(r[this._resourceConfig.columns.find(c => c.primaryKey)!.name]); - console.log(`AutoRemovePlugin: deleted record ${r[this._resourceConfig.columns.find(c => c.primaryKey)!.name]} due to time-based limit`); + await resource.delete(r[this.resource.columns.find(c => c.primaryKey)!.name]); + console.log(`AutoRemovePlugin: deleted record ${r[this.resource.columns.find(c => c.primaryKey)!.name]} due to time-based limit`); } } From 983f8c60bd172e2590c5effd87927144d9bfe025 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Thu, 12 Feb 2026 15:38:58 +0200 Subject: [PATCH 5/9] Add: add plugin option minItemsKeep --- types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/types.ts b/types.ts index 23a4e4c..1549c38 100644 --- a/types.ts +++ b/types.ts @@ -30,6 +30,11 @@ export interface PluginOptions { */ maxItems?: HumanNumber; + /** + * For count-based mode, keep at least X items even if they are older than maxItems (100', '1k', '10k', '1m') + */ + minItemsKeep?: HumanNumber; + /** * Max age of otem for time-based режиму ('1d', '7d', '1mon', '1y') */ From 71a7727788c4909709fdc0e6f7b959117416184b Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Thu, 12 Feb 2026 15:54:39 +0200 Subject: [PATCH 6/9] Fix: validate minItemsKeep against maxItems in count-based mode --- index.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/index.ts b/index.ts index db348b1..ea37442 100644 --- a/index.ts +++ b/index.ts @@ -37,8 +37,15 @@ export default class AutoRemovePlugin extends AdminForthPlugin { } // Check mode-specific options - if (this.options.mode === 'count-based' && !this.options.maxItems) { - throw new Error('maxItems is required for count-based mode'); + if (this.options.mode === 'count-based') { + if (!this.options.maxItems) { + throw new Error('maxItems is required for count-based mode'); + } + if (this.options.minItemsKeep && parseHumanNumber(this.options.minItemsKeep) > parseHumanNumber(this.options.maxItems)) { + throw new Error( + `Option "minItemsKeep" (${this.options.minItemsKeep}) cannot be greater than "maxItems" (${this.options.maxItems}). Please set "minItemsKeep" less than or equal to "maxItems` + ); + } } if (this.options.mode === 'time-based' && !this.options.maxAge) { throw new Error('maxAge is required for time-based mode'); From 14532ae0bbac7c1a311194d029ad3ed917bcceaf Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Fri, 13 Feb 2026 09:16:40 +0200 Subject: [PATCH 7/9] Refactor: remove maxDeletePerRun option and adjust deletion logic in AutoRemovePlugin --- index.ts | 6 ++---- types.ts | 6 ------ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/index.ts b/index.ts index ea37442..67474dd 100644 --- a/index.ts +++ b/index.ts @@ -71,7 +71,7 @@ export default class AutoRemovePlugin extends AdminForthPlugin { const allRecords = await resource.list([], null, null, [Sorts.ASC(this.options.createdAtField)]); if (allRecords.length <= limit) return; - const toDelete = allRecords.slice(0, allRecords.length - limit).slice(0, this.options.maxDeletePerRun); + const toDelete = allRecords.slice(0, allRecords.length - limit); for (const r of toDelete) { await resource.delete(r[this.resource.columns.find(c => c.primaryKey)!.name]); console.log(`AutoRemovePlugin: deleted record ${r[this.resource.columns.find(c => c.primaryKey)!.name]} due to count-based limit`); @@ -84,9 +84,7 @@ export default class AutoRemovePlugin extends AdminForthPlugin { const resource = adminforth.resource(this.resource.resourceId); const allRecords = await resource.list([], null, null, [Sorts.ASC(this.options.createdAtField)]); - const toDelete = allRecords - .filter(r => new Date(r[this.options.createdAtField]).getTime() < threshold) - .slice(0, this.options.maxDeletePerRun); + const toDelete = allRecords.filter(r => new Date(r[this.options.createdAtField]).getTime() < threshold); for (const r of toDelete) { await resource.delete(r[this.resource.columns.find(c => c.primaryKey)!.name]); diff --git a/types.ts b/types.ts index 1549c38..e69fc15 100644 --- a/types.ts +++ b/types.ts @@ -45,10 +45,4 @@ export interface PluginOptions { * Default '1d' */ interval?: HumanDuration; - - /** - * Delete no more than X items per run - * Default 500 - */ - maxDeletePerRun?: number; } From 54500d08107c6370d09b439d29e55afbc06b4849 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Fri, 13 Feb 2026 09:32:16 +0200 Subject: [PATCH 8/9] Fix: rename options maxAge to deleteOlderThan and maxItems to keepAtLeast --- index.ts | 16 ++++++++-------- types.ts | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/index.ts b/index.ts index 67474dd..fa88814 100644 --- a/index.ts +++ b/index.ts @@ -38,17 +38,17 @@ export default class AutoRemovePlugin extends AdminForthPlugin { // Check mode-specific options if (this.options.mode === 'count-based') { - if (!this.options.maxItems) { - throw new Error('maxItems is required for count-based mode'); + if (!this.options.keepAtLeast) { + throw new Error('keepAtLeast is required for count-based mode'); } - if (this.options.minItemsKeep && parseHumanNumber(this.options.minItemsKeep) > parseHumanNumber(this.options.maxItems)) { + if (this.options.minItemsKeep && parseHumanNumber(this.options.minItemsKeep) > parseHumanNumber(this.options.keepAtLeast)) { throw new Error( - `Option "minItemsKeep" (${this.options.minItemsKeep}) cannot be greater than "maxItems" (${this.options.maxItems}). Please set "minItemsKeep" less than or equal to "maxItems` + `Option "minItemsKeep" (${this.options.minItemsKeep}) cannot be greater than "keepAtLeast" (${this.options.keepAtLeast}). Please set "minItemsKeep" less than or equal to "maxItems` ); } } - if (this.options.mode === 'time-based' && !this.options.maxAge) { - throw new Error('maxAge is required for time-based mode'); + if (this.options.mode === 'time-based' && !this.options.deleteOlderThan) { + throw new Error('deleteOlderThan is required for time-based mode'); } } @@ -65,7 +65,7 @@ export default class AutoRemovePlugin extends AdminForthPlugin { } private async cleanupByCount(adminforth: IAdminForth) { - const limit = parseHumanNumber(this.options.maxItems!); + const limit = parseHumanNumber(this.options.keepAtLeast!); const resource = adminforth.resource(this.resource.resourceId); const allRecords = await resource.list([], null, null, [Sorts.ASC(this.options.createdAtField)]); @@ -79,7 +79,7 @@ export default class AutoRemovePlugin extends AdminForthPlugin { } private async cleanupByTime(adminforth: IAdminForth) { - const maxAgeMs = parseDuration(this.options.maxAge!); + const maxAgeMs = parseDuration(this.options.deleteOlderThan!); const threshold = Date.now() - maxAgeMs; const resource = adminforth.resource(this.resource.resourceId); diff --git a/types.ts b/types.ts index e69fc15..d43b4eb 100644 --- a/types.ts +++ b/types.ts @@ -28,7 +28,7 @@ export interface PluginOptions { /** * for count-based mode (100', '1k', '10k', '1m') */ - maxItems?: HumanNumber; + keepAtLeast?: HumanNumber; /** * For count-based mode, keep at least X items even if they are older than maxItems (100', '1k', '10k', '1m') @@ -36,9 +36,9 @@ export interface PluginOptions { minItemsKeep?: HumanNumber; /** - * Max age of otem for time-based режиму ('1d', '7d', '1mon', '1y') + * Max age of item for time-based mode ('1d', '7d', '1mon', '1y') */ - maxAge?: HumanDuration; + deleteOlderThan?: HumanDuration; /** * Interval for running cleanup (e.g. '1h', '1d') From a508c2a900cbab5fad8f86ea449025a6eae588b3 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Fri, 13 Feb 2026 14:36:54 +0200 Subject: [PATCH 9/9] Refactor: optimize deletion logic in cleanupByCount and cleanupByTime methods for batch processing --- index.ts | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/index.ts b/index.ts index fa88814..ca3edae 100644 --- a/index.ts +++ b/index.ts @@ -63,7 +63,7 @@ export default class AutoRemovePlugin extends AdminForthPlugin { console.error('AutoRemovePlugin runCleanup error:', err); } } - + private async cleanupByCount(adminforth: IAdminForth) { const limit = parseHumanNumber(this.options.keepAtLeast!); const resource = adminforth.resource(this.resource.resourceId); @@ -72,10 +72,17 @@ export default class AutoRemovePlugin extends AdminForthPlugin { if (allRecords.length <= limit) return; const toDelete = allRecords.slice(0, allRecords.length - limit); - for (const r of toDelete) { - await resource.delete(r[this.resource.columns.find(c => c.primaryKey)!.name]); - console.log(`AutoRemovePlugin: deleted record ${r[this.resource.columns.find(c => c.primaryKey)!.name]} due to count-based limit`); + + const itemsPerDelete = 100; + + for (let i = 0; i < toDelete.length; i += itemsPerDelete) { + const deletePackage = toDelete.slice(i, i + itemsPerDelete); + await Promise.all( + deletePackage.map(r => resource.delete(r[this.resource.columns.find(c => c.primaryKey)!.name])) + ); } + + console.log(`AutoRemovePlugin: deleted ${toDelete.length} records due to count-based limit`); } private async cleanupByTime(adminforth: IAdminForth) { @@ -86,9 +93,18 @@ export default class AutoRemovePlugin extends AdminForthPlugin { const allRecords = await resource.list([], null, null, [Sorts.ASC(this.options.createdAtField)]); const toDelete = allRecords.filter(r => new Date(r[this.options.createdAtField]).getTime() < threshold); - for (const r of toDelete) { - await resource.delete(r[this.resource.columns.find(c => c.primaryKey)!.name]); - console.log(`AutoRemovePlugin: deleted record ${r[this.resource.columns.find(c => c.primaryKey)!.name]} due to time-based limit`); + const itemsPerDelete = 100; + + for (let i = 0; i < toDelete.length; i += itemsPerDelete) { + const deletePackage = toDelete.slice(i, i + itemsPerDelete); + + await Promise.all( + deletePackage.map(r => resource.delete(r[this.resource.columns.find(c => c.primaryKey)!.name])) + ); + + console.log( + `AutoRemovePlugin: deleted ${deletePackage.length} records due to time-based limit` + ); } }