@adminforth/auto-remove 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+
2
+ #!/bin/bash
3
+
4
+ # write npm run output both to console and to build.log
5
+ npm run build 2>&1 | tee build.log
6
+ build_status=${PIPESTATUS[0]}
7
+
8
+ # if exist status from the npm run build is not 0
9
+ # then exit with the status code from the npm run build
10
+ if [ $build_status -ne 0 ]; then
11
+ echo "Build failed. Exiting with status code $build_status"
12
+ exit $build_status
13
+ fi
@@ -0,0 +1,46 @@
1
+ #!/bin/sh
2
+
3
+ set -x
4
+
5
+ COMMIT_SHORT_SHA=$(echo $CI_COMMIT_SHA | cut -c1-8)
6
+
7
+ STATUS=${1}
8
+
9
+
10
+ if [ "$STATUS" = "success" ]; then
11
+ MESSAGE="Did a build without issues on \`$CI_REPO_NAME/$CI_COMMIT_BRANCH\`. Commit: _${CI_COMMIT_MESSAGE}_ (<$CI_COMMIT_URL|$COMMIT_SHORT_SHA>)"
12
+
13
+ curl -s -X POST -H "Content-Type: application/json" -d '{
14
+ "username": "'"$CI_COMMIT_AUTHOR"'",
15
+ "icon_url": "'"$CI_COMMIT_AUTHOR_AVATAR"'",
16
+ "attachments": [
17
+ {
18
+ "mrkdwn_in": ["text", "pretext"],
19
+ "color": "#36a64f",
20
+ "text": "'"$MESSAGE"'"
21
+ }
22
+ ]
23
+ }' "$DEVELOPERS_SLACK_WEBHOOK"
24
+ exit 0
25
+ fi
26
+ export BUILD_LOG=$(cat ./build.log)
27
+
28
+ BUILD_LOG=$(echo $BUILD_LOG | sed 's/"/\\"/g')
29
+
30
+ MESSAGE="Broke \`$CI_REPO_NAME/$CI_COMMIT_BRANCH\` with commit _${CI_COMMIT_MESSAGE}_ (<$CI_COMMIT_URL|$COMMIT_SHORT_SHA>)"
31
+ CODE_BLOCK="\`\`\`$BUILD_LOG\n\`\`\`"
32
+
33
+ echo "Sending slack message to developers $MESSAGE"
34
+ # Send the message
35
+ curl -sS -X POST -H "Content-Type: application/json" -d '{
36
+ "username": "'"$CI_COMMIT_AUTHOR"'",
37
+ "icon_url": "'"$CI_COMMIT_AUTHOR_AVATAR"'",
38
+ "attachments": [
39
+ {
40
+ "mrkdwn_in": ["text", "pretext"],
41
+ "color": "#8A1C12",
42
+ "text": "'"$CODE_BLOCK"'",
43
+ "pretext": "'"$MESSAGE"'"
44
+ }
45
+ ]
46
+ }' "$DEVELOPERS_SLACK_WEBHOOK" 2>&1
@@ -0,0 +1,57 @@
1
+ clone:
2
+ git:
3
+ image: woodpeckerci/plugin-git
4
+ settings:
5
+ partial: false
6
+ depth: 5
7
+
8
+ steps:
9
+ init-secrets:
10
+ when:
11
+ - event: push
12
+ image: infisical/cli
13
+ environment:
14
+ INFISICAL_TOKEN:
15
+ from_secret: VAULT_TOKEN
16
+ commands:
17
+ - infisical export --domain https://vault.devforth.io/api --format=dotenv-export --env="prod" > /woodpecker/deploy.vault.env
18
+
19
+ build:
20
+ image: devforth/node20-pnpm:latest
21
+ when:
22
+ - event: push
23
+ commands:
24
+ - apt update && apt install -y rsync
25
+ - . /woodpecker/deploy.vault.env
26
+ - pnpm install
27
+ - /bin/bash ./.woodpecker/buildRelease.sh
28
+ - npm audit signatures
29
+
30
+ release:
31
+ image: devforth/node20-pnpm:latest
32
+ when:
33
+ - event:
34
+ - push
35
+ branch:
36
+ - main
37
+ commands:
38
+ - . /woodpecker/deploy.vault.env
39
+ - pnpm exec semantic-release
40
+
41
+ slack-on-failure:
42
+ image: curlimages/curl
43
+ when:
44
+ - event: push
45
+ status: [failure]
46
+ commands:
47
+ - . /woodpecker/deploy.vault.env
48
+ - /bin/sh ./.woodpecker/buildSlackNotify.sh failure
49
+
50
+ slack-on-success:
51
+ image: curlimages/curl
52
+ when:
53
+ - event: push
54
+ status: [success]
55
+ commands:
56
+ - . /woodpecker/deploy.vault.env
57
+ - /bin/sh ./.woodpecker/buildSlackNotify.sh success
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # Auto Remove Plugin
2
+
3
+ This plugin removes records from resources based on **count-based** or **time-based** rules.
4
+
5
+ It is designed for cleaning up:
6
+
7
+ * old records
8
+ * logs
9
+ * demo/test data
10
+ * temporary entities
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ To install the plugin:
17
+
18
+ ```ts
19
+ npm install @adminforth/auto-remove
20
+ ```
21
+
22
+ Import it into your resource:
23
+ ```ts
24
+ import AutoRemovePlugin from '../../plugins/adminforth-auto-remove/index.js';
25
+ ```
26
+
27
+ ## Plugin Options
28
+
29
+ ```ts
30
+ export interface PluginOptions {
31
+ createdAtField: string;
32
+
33
+ /**
34
+ * - count-based: Delete items > keepAtLeast
35
+ * - time-based: Delete age > deleteOlderThan
36
+ */
37
+ mode: AutoRemoveMode;
38
+
39
+ /**
40
+ * for count-based mode (100', '1k', '10k', '1m')
41
+ */
42
+ keepAtLeast?: HumanNumber;
43
+
44
+ /**
45
+ * Minimum number of items to always keep in count-based mode.
46
+ * This acts as a safety threshold together with `keepAtLeast`.
47
+ * Example formats: '100', '1k', '10k', '1m'.
48
+ *
49
+ * Validation ensures that minItemsKeep <= keepAtLeast.
50
+ */
51
+ minItemsKeep?: HumanNumber;
52
+
53
+ /**
54
+ * Max age of item for time-based mode ('1d', '7d', '1mo', '1y')
55
+ */
56
+ deleteOlderThan?: HumanDuration;
57
+
58
+ /**
59
+ * Interval for running cleanup (e.g. '1h', '1d')
60
+ * Default '1d'
61
+ */
62
+ interval?: HumanDuration;
63
+ }
64
+ ```
65
+ ---
66
+
67
+ ## Usage
68
+ To use the plugin, add it to your resource file. Here's an example:
69
+
70
+ for count-based mode
71
+ ```ts
72
+ new AutoRemovePlugin({
73
+ createdAtField: 'created_at',
74
+ mode: 'count-based',
75
+ keepAtLeast: '200',
76
+ interval: '1mo',
77
+ minItemsKeep: '180',
78
+ }),
79
+ ```
80
+
81
+ for time-based mode
82
+ ```ts
83
+ new AutoRemovePlugin({
84
+ createdAtField: 'created_at',
85
+ mode: 'time-based',
86
+ deleteOlderThan: '3mo',
87
+ interval: '1mo',
88
+ }),
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Result
94
+ After running **AutoRemovePlugin**, old or excess records are deleted automatically:
95
+
96
+ - **Count-based mode:** keeps the newest `keepAtLeast` records, deletes older ones.
97
+ Example: `keepAtLeast = 500` → table with 650 records deletes 150 oldest.
98
+
99
+ - **Time-based mode:** deletes records older than `deleteOlderThan`.
100
+ Example: `deleteOlderThan = '7d'` → removes records older than 7 days.
101
+
102
+ - **Manual cleanup:** `POST /plugin/{pluginInstanceId}/cleanup`, returns `{ "ok": true }`.
103
+
104
+ Logs show how many records were removed per run.
package/build.log ADDED
@@ -0,0 +1,10 @@
1
+
2
+ > @adminforth/auto-remove@1.0.0 build
3
+ > tsc && rsync -av --exclude 'node_modules' custom dist/
4
+
5
+ sending incremental file list
6
+ custom/
7
+ custom/tsconfig.json
8
+
9
+ sent 451 bytes received 39 bytes 980.00 bytes/sec
10
+ total size is 308 speedup is 0.63
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".", // This should point to your project root
4
+ "paths": {
5
+ "@/*": [
6
+ "../node_modules/adminforth/dist/spa/src/*"
7
+ ],
8
+ "*": [
9
+ "../node_modules/adminforth/dist/spa/node_modules/*"
10
+ ],
11
+ "@@/*": [
12
+ "."
13
+ ]
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".", // This should point to your project root
4
+ "paths": {
5
+ "@/*": [
6
+ "../node_modules/adminforth/dist/spa/src/*"
7
+ ],
8
+ "*": [
9
+ "../node_modules/adminforth/dist/spa/node_modules/*"
10
+ ],
11
+ "@@/*": [
12
+ "."
13
+ ]
14
+ }
15
+ }
16
+ }
package/dist/index.js ADDED
@@ -0,0 +1,119 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { AdminForthPlugin, Sorts, AdminForthDataTypes } from "adminforth";
11
+ import { parseHumanNumber } from './utils/parseNumber.js';
12
+ import { parseDuration } from './utils/parseDuration.js';
13
+ export default class AutoRemovePlugin extends AdminForthPlugin {
14
+ constructor(options) {
15
+ super(options, import.meta.url);
16
+ this.options = options;
17
+ this.shouldHaveSingleInstancePerWholeApp = () => false;
18
+ }
19
+ instanceUniqueRepresentation(pluginOptions) {
20
+ return `single`;
21
+ }
22
+ modifyResourceConfig(adminforth, resourceConfig) {
23
+ const _super = Object.create(null, {
24
+ modifyResourceConfig: { get: () => super.modifyResourceConfig }
25
+ });
26
+ return __awaiter(this, void 0, void 0, function* () {
27
+ _super.modifyResourceConfig.call(this, adminforth, resourceConfig);
28
+ if (resourceConfig) {
29
+ this.resource = resourceConfig;
30
+ }
31
+ const intervalMs = parseDuration(this.options.interval || '1d');
32
+ this.timer = setInterval(() => {
33
+ this.runCleanup(adminforth).catch(console.error);
34
+ }, intervalMs);
35
+ });
36
+ }
37
+ validateConfigAfterDiscover(adminforth, resourceConfig) {
38
+ const col = resourceConfig.columns.find(c => c.name === this.options.createdAtField);
39
+ if (!col)
40
+ throw new Error(`Field "${this.options.createdAtField}" not found in resource "${resourceConfig.label}", but required`);
41
+ if (![AdminForthDataTypes.DATE, AdminForthDataTypes.DATETIME].includes(col.type)) {
42
+ throw new Error(`Field "${this.options.createdAtField}" in resource "${resourceConfig.label}" must be of type DATE or DATETIME`);
43
+ }
44
+ if (this.options.mode !== 'time-based' && this.options.mode !== 'count-based') {
45
+ throw new Error(`wrong delete mode "${this.options.mode}", please set "time-based" or "count-based"`);
46
+ }
47
+ if (this.options.mode === 'count-based') {
48
+ if (!this.options.keepAtLeast) {
49
+ throw new Error('keepAtLeast is required for count-based mode');
50
+ }
51
+ if (this.options.minItemsKeep && parseHumanNumber(this.options.minItemsKeep) > parseHumanNumber(this.options.keepAtLeast)) {
52
+ throw new Error(`Option "minItemsKeep" (${this.options.minItemsKeep}) cannot be greater than "keepAtLeast" (${this.options.keepAtLeast}). Please set "minItemsKeep" less than or equal to "keepAtLeast"`);
53
+ }
54
+ }
55
+ if (this.options.mode === 'time-based' && !this.options.deleteOlderThan) {
56
+ throw new Error('deleteOlderThan is required for time-based mode');
57
+ }
58
+ if (this.options.mode === 'count-based' && !this.options.minItemsKeep) {
59
+ throw new Error('minItemsKeep is required');
60
+ }
61
+ }
62
+ runCleanup(adminforth) {
63
+ return __awaiter(this, void 0, void 0, function* () {
64
+ try {
65
+ if (this.options.mode === 'count-based') {
66
+ yield this.cleanupByCount(adminforth, this.resourceConfig);
67
+ }
68
+ else {
69
+ yield this.cleanupByTime(adminforth, this.resourceConfig);
70
+ }
71
+ }
72
+ catch (err) {
73
+ console.error('AutoRemovePlugin runCleanup error:', err);
74
+ }
75
+ });
76
+ }
77
+ cleanupByCount(adminforth, resourceConfig) {
78
+ return __awaiter(this, void 0, void 0, function* () {
79
+ const limit = parseHumanNumber(this.options.keepAtLeast);
80
+ const resource = adminforth.resource(this.resource.resourceId);
81
+ const allRecords = yield resource.list([], null, null, [Sorts.ASC(this.options.createdAtField)]);
82
+ if (allRecords.length <= limit)
83
+ return;
84
+ const toDelete = allRecords.slice(0, allRecords.length - limit);
85
+ const pkColumn = this.resource.columns.find(c => c.primaryKey).name;
86
+ const ids = toDelete.map(r => r[pkColumn]);
87
+ yield resource.dataConnector.deleteMany({ resource: resourceConfig, recordIds: ids });
88
+ console.log(`AutoRemovePlugin: deleted ${toDelete.length} records due to count-based limit`);
89
+ });
90
+ }
91
+ cleanupByTime(adminforth, resourceConfig) {
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ const maxAgeMs = parseDuration(this.options.deleteOlderThan);
94
+ const threshold = Date.now() - maxAgeMs;
95
+ const resource = adminforth.resource(this.resource.resourceId);
96
+ const allRecords = yield resource.list([], null, null, [Sorts.ASC(this.options.createdAtField)]);
97
+ const toDelete = allRecords.filter(r => new Date(r[this.options.createdAtField]).getTime() < threshold);
98
+ const pkColumn = this.resource.columns.find(c => c.primaryKey).name;
99
+ const ids = toDelete.map(r => r[pkColumn]);
100
+ yield resource.dataConnector.deleteMany({ resource: resourceConfig, recordIds: ids });
101
+ console.log(`AutoRemovePlugin: deleted ${toDelete.length} records due to time-based limit`);
102
+ });
103
+ }
104
+ setupEndpoints(server) {
105
+ server.endpoint({
106
+ method: 'POST',
107
+ path: `/plugin/${this.pluginInstanceId}/cleanup`,
108
+ handler: () => __awaiter(this, void 0, void 0, function* () {
109
+ try {
110
+ yield this.runCleanup(this.adminforth);
111
+ return { ok: true };
112
+ }
113
+ catch (err) {
114
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
115
+ }
116
+ })
117
+ });
118
+ }
119
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ const UNITS = {
2
+ s: 1000,
3
+ m: 60000,
4
+ h: 3600000,
5
+ d: 86400000,
6
+ w: 604800000,
7
+ mo: 2592000000,
8
+ y: 31536000000,
9
+ };
10
+ export function parseDuration(value) {
11
+ const match = value.match(/^(\d+)\s*(s|m|h|d|w|mo|y)$/);
12
+ if (!match) {
13
+ throw new Error(`Invalid duration format: ${value}`);
14
+ }
15
+ const [, amount, unit] = match;
16
+ return Number(amount) * UNITS[unit];
17
+ }
@@ -0,0 +1,10 @@
1
+ export function parseHumanNumber(value) {
2
+ const v = value.toLowerCase().trim();
3
+ if (v.endsWith('kk'))
4
+ return Number(v.slice(0, -2)) * 1000000;
5
+ if (v.endsWith('k'))
6
+ return Number(v.slice(0, -1)) * 1000;
7
+ if (v.endsWith('m'))
8
+ return Number(v.slice(0, -1)) * 1000000;
9
+ return Number(v);
10
+ }
package/index.ts ADDED
@@ -0,0 +1,119 @@
1
+ import { AdminForthPlugin, Filters, Sorts, AdminForthDataTypes } from "adminforth";
2
+ import type { IAdminForth, IHttpServer, AdminForthResource } from "adminforth";
3
+ import type { PluginOptions } from './types.js';
4
+ import { parseHumanNumber } from './utils/parseNumber.js';
5
+ import { parseDuration } from './utils/parseDuration.js';
6
+
7
+ export default class AutoRemovePlugin extends AdminForthPlugin {
8
+ options: PluginOptions;
9
+ resource?: AdminForthResource;
10
+ timer?: NodeJS.Timeout;
11
+
12
+ constructor(options: PluginOptions) {
13
+ super(options, import.meta.url);
14
+ this.options = options;
15
+ this.shouldHaveSingleInstancePerWholeApp = () => false;
16
+ }
17
+
18
+ instanceUniqueRepresentation(pluginOptions: any) : string {
19
+ return `single`;
20
+ }
21
+
22
+ async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
23
+ super.modifyResourceConfig(adminforth, resourceConfig);
24
+
25
+ if (resourceConfig) {
26
+ this.resource = resourceConfig;
27
+ }
28
+
29
+ const intervalMs = parseDuration(this.options.interval || '1d');
30
+ this.timer = setInterval(() => {
31
+ this.runCleanup(adminforth).catch(console.error);
32
+ }, intervalMs);
33
+ }
34
+
35
+ validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
36
+ const col = resourceConfig.columns.find(c => c.name === this.options.createdAtField);
37
+ if (!col) throw new Error(`Field "${this.options.createdAtField}" not found in resource "${resourceConfig.label}", but required`);
38
+ if (![AdminForthDataTypes.DATE, AdminForthDataTypes.DATETIME].includes(col.type!)) {
39
+ throw new Error(`Field "${this.options.createdAtField}" in resource "${resourceConfig.label}" must be of type DATE or DATETIME`);
40
+ }
41
+ if (this.options.mode !== 'time-based' && this.options.mode !== 'count-based'){
42
+ throw new Error(`wrong delete mode "${this.options.mode}", please set "time-based" or "count-based"`);
43
+ }
44
+ if (this.options.mode === 'count-based') {
45
+ if (!this.options.keepAtLeast) {
46
+ throw new Error('keepAtLeast is required for count-based mode');
47
+ }
48
+ if (this.options.minItemsKeep && parseHumanNumber(this.options.minItemsKeep) > parseHumanNumber(this.options.keepAtLeast)) {
49
+ throw new Error(
50
+ `Option "minItemsKeep" (${this.options.minItemsKeep}) cannot be greater than "keepAtLeast" (${this.options.keepAtLeast}). Please set "minItemsKeep" less than or equal to "keepAtLeast"`
51
+ );
52
+ }
53
+ }
54
+ if (this.options.mode === 'time-based' && !this.options.deleteOlderThan) {
55
+ throw new Error('deleteOlderThan is required for time-based mode');
56
+ }
57
+ if (this.options.mode === 'count-based' && !this.options.minItemsKeep){
58
+ throw new Error('minItemsKeep is required');
59
+ }
60
+ }
61
+
62
+ private async runCleanup(adminforth: IAdminForth) {
63
+ try {
64
+ if (this.options.mode === 'count-based') {
65
+ await this.cleanupByCount(adminforth, this.resourceConfig);
66
+ } else {
67
+ await this.cleanupByTime(adminforth, this.resourceConfig);
68
+ }
69
+ } catch (err) {
70
+ console.error('AutoRemovePlugin runCleanup error:', err);
71
+ }
72
+ }
73
+
74
+ private async cleanupByCount(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
75
+ const limit = parseHumanNumber(this.options.keepAtLeast!);
76
+ const resource = adminforth.resource(this.resource.resourceId);
77
+
78
+ const allRecords = await resource.list([], null, null, [Sorts.ASC(this.options.createdAtField)]);
79
+ if (allRecords.length <= limit) return;
80
+
81
+ const toDelete = allRecords.slice(0, allRecords.length - limit);
82
+ const pkColumn = this.resource.columns.find(c => c.primaryKey)!.name;
83
+ const ids = toDelete.map(r => r[pkColumn]);
84
+
85
+ await resource.dataConnector.deleteMany({ resource: resourceConfig, recordIds: ids });
86
+
87
+ console.log(`AutoRemovePlugin: deleted ${toDelete.length} records due to count-based limit`);
88
+ }
89
+
90
+ private async cleanupByTime(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
91
+ const maxAgeMs = parseDuration(this.options.deleteOlderThan!);
92
+ const threshold = Date.now() - maxAgeMs;
93
+ const resource = adminforth.resource(this.resource.resourceId);
94
+
95
+ const allRecords = await resource.list([], null, null, [Sorts.ASC(this.options.createdAtField)]);
96
+ const toDelete = allRecords.filter(r => new Date(r[this.options.createdAtField]).getTime() < threshold);
97
+
98
+ const pkColumn = this.resource.columns.find(c => c.primaryKey)!.name;
99
+ const ids = toDelete.map(r => r[pkColumn]);
100
+ await resource.dataConnector.deleteMany({ resource: resourceConfig, recordIds: ids });
101
+
102
+ console.log(`AutoRemovePlugin: deleted ${toDelete.length} records due to time-based limit`);
103
+ }
104
+
105
+ setupEndpoints(server: IHttpServer) {
106
+ server.endpoint({
107
+ method: 'POST',
108
+ path: `/plugin/${this.pluginInstanceId}/cleanup`,
109
+ handler: async () => {
110
+ try {
111
+ await this.runCleanup(this.adminforth);
112
+ return { ok: true };
113
+ } catch (err) {
114
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
115
+ }
116
+ }
117
+ });
118
+ }
119
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@adminforth/auto-remove",
3
+ "version": "1.0.2",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc && rsync -av --exclude 'node_modules' custom dist/"
12
+ },
13
+ "keywords": [],
14
+ "author": "",
15
+ "license": "ISC",
16
+ "description": "",
17
+ "devDependencies": {
18
+ "@types/node": "latest",
19
+ "semantic-release": "^24.2.1",
20
+ "semantic-release-slack-bot": "^4.0.2",
21
+ "typescript": "^5.7.3"
22
+ },
23
+ "peerDependencies": {
24
+ "adminforth": "2.25.4"
25
+ },
26
+ "release": {
27
+ "plugins": [
28
+ "@semantic-release/commit-analyzer",
29
+ "@semantic-release/release-notes-generator",
30
+ "@semantic-release/npm",
31
+ "@semantic-release/github",
32
+ [
33
+ "semantic-release-slack-bot",
34
+ {
35
+ "packageName": "@adminforth/auto-remove",
36
+ "notifyOnSuccess": true,
37
+ "notifyOnFail": true,
38
+ "slackIcon": ":package:",
39
+ "markdownReleaseNotes": true
40
+ }
41
+ ]
42
+ ],
43
+ "branches": [
44
+ "main",
45
+ {
46
+ "name": "next",
47
+ "prerelease": true
48
+ }
49
+ ]
50
+ }
51
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include*/
4
+ "module": "node16", /* Specify what module code is generated. */
5
+ "outDir": "./dist", /* Specify an output folder for all emitted files. */
6
+ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. */
7
+ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
8
+ "strict": false, /* Enable all strict type-checking options. */
9
+ "skipLibCheck": true, /* Skip type checking all .d.ts files. */
10
+ },
11
+ "exclude": ["node_modules", "dist", "custom"], /* Exclude files from compilation. */
12
+ }
13
+
package/types.ts ADDED
@@ -0,0 +1,52 @@
1
+ export type AutoRemoveMode = 'count-based' | 'time-based';
2
+
3
+ /**
4
+ * count-based 100, 1k, 1kk, 1m
5
+ */
6
+ export type HumanNumber = string;
7
+
8
+ /**
9
+ * s
10
+ * min
11
+ * h
12
+ * d
13
+ * w
14
+ * mon
15
+ * y
16
+ */
17
+ export type HumanDuration = string;
18
+
19
+ export interface PluginOptions {
20
+ createdAtField: string;
21
+
22
+ /**
23
+ * - count-based: Delete items > maxItems
24
+ * - time-based: Delete age > maxAge
25
+ */
26
+ mode: AutoRemoveMode;
27
+
28
+ /**
29
+ * for count-based mode (100', '1k', '10k', '1m')
30
+ */
31
+ keepAtLeast?: HumanNumber;
32
+
33
+ /**
34
+ * Minimum number of items to always keep in count-based mode.
35
+ * This acts as a safety threshold together with `keepAtLeast`.
36
+ * Example formats: '100', '1k', '10k', '1m'.
37
+ *
38
+ * Validation ensures that minItemsKeep <= keepAtLeast.
39
+ */
40
+ minItemsKeep?: HumanNumber;
41
+
42
+ /**
43
+ * Max age of item for time-based mode ('1d', '7d', '1mo', '1y')
44
+ */
45
+ deleteOlderThan?: HumanDuration;
46
+
47
+ /**
48
+ * Interval for running cleanup (e.g. '1h', '1d')
49
+ * Default '1d'
50
+ */
51
+ interval?: HumanDuration;
52
+ }
@@ -0,0 +1,20 @@
1
+ const UNITS: Record<string, number> = {
2
+ s: 1000,
3
+ m: 60_000,
4
+ h: 3_600_000,
5
+ d: 86_400_000,
6
+ w: 604_800_000,
7
+ mo: 2_592_000_000,
8
+ y: 31_536_000_000,
9
+ };
10
+
11
+ export function parseDuration(value: string): number {
12
+ const match = value.match(/^(\d+)\s*(s|m|h|d|w|mo|y)$/);
13
+ if (!match) {
14
+ throw new Error(`Invalid duration format: ${value}`);
15
+ }
16
+
17
+ const [, amount, unit] = match;
18
+ return Number(amount) * UNITS[unit];
19
+ }
20
+
@@ -0,0 +1,9 @@
1
+ export function parseHumanNumber(value: string): number {
2
+ const v = value.toLowerCase().trim();
3
+
4
+ if (v.endsWith('kk')) return Number(v.slice(0, -2)) * 1_000_000;
5
+ if (v.endsWith('k')) return Number(v.slice(0, -1)) * 1_000;
6
+ if (v.endsWith('m')) return Number(v.slice(0, -1)) * 1_000_000;
7
+
8
+ return Number(v);
9
+ }