Skip to content

Commit bfad936

Browse files
committed
docs: add docs for background jobs plugin
1 parent 1c52687 commit bfad936

2 files changed

Lines changed: 397 additions & 1 deletion

File tree

Lines changed: 396 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
1+
# Background jobs
2+
BackgroundJobsPlugin adds a durable background-job system to AdminForth. Jobs are stored in your data store (via a resource), executed by registered handlers, and automatically resumed after server restarts.
3+
4+
5+
## Setup
6+
7+
First, install the plugin:
8+
```bash
9+
npm i @adminforth/background-jobs
10+
```
11+
12+
and create a resource for jobs:
13+
14+
15+
```ts title="./resources/jobs.ts"
16+
import AdminForth, { AdminForthDataTypes } from 'adminforth';
17+
import type { AdminForthResourceInput, AdminUser } from 'adminforth';
18+
import { randomUUID } from 'crypto';
19+
import BackgroundJobsPlugin from '@adminforth/background-jobs';
20+
21+
async function allowedForSuperAdmin({ adminUser }: { adminUser: AdminUser }): Promise<boolean> {
22+
return adminUser.dbUser.role === 'superadmin';
23+
}
24+
25+
export default {
26+
dataSource: 'maindb',
27+
table: 'jobs',
28+
resourceId: 'jobs',
29+
label: 'Jobs',
30+
options: {
31+
allowedActions: {
32+
edit: allowedForSuperAdmin,
33+
delete: allowedForSuperAdmin,
34+
},
35+
},
36+
columns: [
37+
{
38+
name: 'id',
39+
primaryKey: true,
40+
type: AdminForthDataTypes.STRING,
41+
fillOnCreate: ({ initialRecord, adminUser }) => randomUUID(),
42+
showIn: {
43+
edit: false,
44+
create: false,
45+
},
46+
},
47+
{
48+
name: 'name',
49+
type: AdminForthDataTypes.STRING,
50+
},
51+
{
52+
name: 'created_at',
53+
type: AdminForthDataTypes.DATETIME,
54+
showIn: {
55+
edit: false,
56+
create: false,
57+
},
58+
fillOnCreate: ({ initialRecord, adminUser }) => (new Date()).toISOString(),
59+
},
60+
{
61+
name: 'finished_at',
62+
type: AdminForthDataTypes.DATETIME,
63+
showIn: {
64+
edit: false,
65+
create: false,
66+
},
67+
},
68+
{
69+
name: 'started_by',
70+
type: AdminForthDataTypes.STRING,
71+
foreignResource: {
72+
resourceId: 'adminuser',
73+
searchableFields: ["id", "email"],
74+
}
75+
},
76+
{
77+
name: 'state',
78+
type: AdminForthDataTypes.JSON,
79+
},
80+
{
81+
name: 'progress',
82+
type: AdminForthDataTypes.STRING,
83+
},
84+
{
85+
name: 'status',
86+
type: AdminForthDataTypes.STRING,
87+
enum: [
88+
{
89+
label: 'IN_PROGRESS',
90+
value: 'IN_PROGRESS',
91+
},
92+
{
93+
label: 'DONE',
94+
value: 'DONE',
95+
},
96+
{
97+
label: 'DONE_WITH_ERRORS',
98+
value: 'DONE_WITH_ERRORS',
99+
},
100+
{
101+
label: 'CANCELLED',
102+
value: 'CANCELLED',
103+
}
104+
]
105+
},
106+
{
107+
name: 'job_handler_name',
108+
type: AdminForthDataTypes.STRING,
109+
},
110+
],
111+
plugins: [
112+
new BackgroundJobsPlugin({
113+
createdAtField: 'created_at',
114+
finishedAtField: 'finished_at',
115+
startedByField: 'started_by',
116+
stateField: 'state',
117+
progressField: 'progress',
118+
statusField: 'status',
119+
nameField: 'name',
120+
jobHandlerField: 'job_handler_name',
121+
})
122+
]
123+
} as AdminForthResourceInput;
124+
```
125+
126+
## Usage
127+
The plugin saves tasks and keeps executing them even after a server restart, so you should register job task handlers at the start of the AdminForth application.
128+
129+
```ts title="./index.ts"
130+
//diff-add
131+
import BackgroundJobsPlugin from '@adminforth/background-jobs';
132+
133+
...
134+
135+
admin.express.serve(app);
136+
137+
admin.discoverDatabases().then(async () => {
138+
if (await admin.resource('adminuser').count() === 0) {
139+
await admin.resource('adminuser').create({
140+
email: 'adminforth',
141+
password_hash: await AdminForth.Utils.generatePasswordHash('adminforth'),
142+
role: 'superadmin',
143+
});
144+
}
145+
});
146+
147+
//diff-add
148+
const backgroundJobsPlugin = admin.getPluginByClassName<BackgroundJobsPlugin>('BackgroundJobsPlugin');
149+
150+
//diff-add
151+
backgroundJobsPlugin.registerTaskHandler({
152+
//diff-add
153+
// job handler name
154+
//diff-add
155+
jobHandlerName: 'example_job_handler',
156+
//diff-add
157+
//handler function
158+
//diff-add
159+
handler: async ({ setTaskStateField, getTaskStateField }) => {
160+
//diff-add
161+
const state = await getTaskStateField();
162+
//diff-add
163+
console.log('State of the task at the beginning of the job handler:', state);
164+
//diff-add
165+
await new Promise(resolve => setTimeout(resolve, 3000));
166+
//diff-add
167+
await setTaskStateField({[state.step]: `Step ${state.step} completed`});
168+
//diff-add
169+
const updatedState = await getTaskStateField();
170+
//diff-add
171+
console.log('State of the task after setting the new state in the job handler:', updatedState);
172+
//diff-add
173+
},
174+
//diff-add
175+
//limit of tasks, that are running in parallel
176+
//diff-add
177+
parallelLimit: 1
178+
//diff-add
179+
})
180+
181+
...
182+
183+
```
184+
185+
186+
After registering a handler, you can create a job. For example:
187+
188+
189+
```ts title="./index.ts"
190+
191+
...
192+
193+
if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
194+
const app = express();
195+
app.use(express.json());
196+
197+
//diff-add
198+
app.post(`${ADMIN_BASE_URL}/api/create-job/`,
199+
//diff-add
200+
admin.express.authorize(
201+
//diff-add
202+
async (req: any, res: any) => {
203+
//diff-add
204+
const backgroundJobsPlugin = admin.getPluginByClassName<BackgroundJobsPlugin>('default');
205+
//diff-add
206+
if (!backgroundJobsPlugin) {
207+
//diff-add
208+
res.status(404).json({ error: 'BackgroundJobsPlugin not found' });
209+
//diff-add
210+
return;
211+
//diff-add
212+
}
213+
//diff-add
214+
backgroundJobsPlugin.startNewJob(
215+
//diff-add
216+
'Example Job', //job name
217+
//diff-add
218+
req.adminUser, // adminuser
219+
//diff-add
220+
[
221+
//diff-add
222+
{ state: { step: 1 } },
223+
//diff-add
224+
{ state: { step: 2 } },
225+
//diff-add
226+
{ state: { step: 3 } },
227+
//diff-add
228+
{ state: { step: 4 } },
229+
//diff-add
230+
{ state: { step: 5 } },
231+
//diff-add
232+
{ state: { step: 6 } },
233+
//diff-add
234+
], //initial tasks
235+
//diff-add
236+
'example_job_handler', //job handler name
237+
//diff-add
238+
)
239+
//diff-add
240+
res.json({ok: true, message: 'Job started' });
241+
//diff-add
242+
}
243+
//diff-add
244+
),
245+
//diff-add
246+
);
247+
248+
...
249+
250+
```
251+
## Custom job state renderer
252+
There may be cases when you need to display the state of job tasks. For this, you can register a custom component.
253+
254+
255+
```ts title="./custom/JobCustomComponent.vue"
256+
<template>
257+
<div class="w-[1000px] h-[500px] bg-gray-100 rounded-lg p-4 flex flex-col items-center justify-center ">
258+
<Button class="h-10" @click="loadTasks">
259+
Get Job Tasks
260+
</Button>
261+
{{ tasks }}
262+
</div>
263+
</template>
264+
265+
266+
<script setup lang="ts">
267+
import { Button, JsonViewer } from '@/afcl';
268+
import { onMounted, onUnmounted, ref } from 'vue';
269+
import websocket from '@/websocket';
270+
import type { AdminForthComponentDeclarationFull } from 'adminforth';
271+
272+
273+
const tasks = ref<{state: Record<string, any>, status: string}[]>([]);
274+
275+
276+
const props = defineProps<{
277+
meta: any;
278+
getJobTasks: (limit?: number, offset?: number) => Promise<{state: Record<string, any>, status: string}[]>;
279+
job: {
280+
id: string;
281+
name: string;
282+
status: 'IN_PROGRESS' | 'DONE' | 'DONE_WITH_ERRORS' | 'CANCELLED';
283+
progress: number; // 0 to 100
284+
createdAt: Date;
285+
customComponent?: AdminForthComponentDeclarationFull;
286+
};
287+
}>();
288+
289+
const loadTasks = async () => {
290+
tasks.value = await props.getJobTasks(10, 0);
291+
console.log('Loaded tasks for job:', tasks.value);
292+
}
293+
294+
295+
onMounted(async () => {
296+
loadTasks();
297+
websocket.subscribe(`/background-jobs-task-update/${props.job.id}`, (data: { taskIndex: number, status?: string, state?: Record<string, any> }) => {
298+
console.log('Received WebSocket message for job:', data.status);
299+
300+
if (data.state) {
301+
tasks.value[data.taskIndex].state = data.state;
302+
}
303+
if (data.status) {
304+
tasks.value[data.taskIndex].status = data.status;
305+
}
306+
307+
});
308+
});
309+
310+
onUnmounted(() => {
311+
console.log('Unsubscribing from WebSocket for job:', props.job.id);
312+
websocket.unsubscribe(`/background-jobs-task-update/${props.job.id}`);
313+
});
314+
315+
316+
</script>
317+
```
318+
319+
320+
Now register this component explicitly:
321+
322+
```ts title="./index.ts"
323+
export const admin = new AdminForth({
324+
baseUrl: ADMIN_BASE_URL,
325+
auth: {
326+
usersResourceId: 'adminuser',
327+
usernameField: 'email',
328+
passwordHashField: 'password_hash',
329+
rememberMeDuration: '30d',
330+
loginBackgroundImage: 'https://images.unsplash.com/photo-1534239697798-120952b76f2b?q=80&w=3389&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
331+
loginBackgroundPosition: '1/2',
332+
loginPromptHTML: async () => {
333+
const adminforthUserExists = await admin.resource("adminuser").count(Filters.EQ('email', 'adminforth')) > 0;
334+
if (adminforthUserExists) {
335+
return "Please use <b>adminforth</b> as username and <b>adminforth</b> as password"
336+
}
337+
},
338+
},
339+
340+
//diff-add
341+
componentsToExplicitRegister: [
342+
//diff-add
343+
{
344+
//diff-add
345+
file: '@@/JobCustomComponent.vue',
346+
//diff-add
347+
meta: {
348+
//diff-add
349+
label: 'Job Custom Component',
350+
//diff-add
351+
}
352+
//diff-add
353+
}
354+
//diff-add
355+
],
356+
...
357+
358+
```
359+
360+
361+
Finally, register this component alongside the job task handler:
362+
363+
```ts title="./index.ts"
364+
...
365+
366+
const backgroundJobsPlugin = admin.getPluginByClassName<BackgroundJobsPlugin>('default');
367+
368+
backgroundJobsPlugin.registerTaskHandler({
369+
jobHandlerName: 'example_job_handler', // Handler name
370+
handler: async ({ setTaskStateField, getTaskStateField }) => { //handler function
371+
const state = await getTaskStateField();
372+
console.log('State of the task at the beginning of the job handler:', state);
373+
await new Promise(resolve => setTimeout(resolve, 3000));
374+
await setTaskStateField({[state.step]: `Step ${state.step} completed`});
375+
const updatedState = await getTaskStateField();
376+
console.log('State of the task after setting the new state in the job handler:', updatedState);
377+
},
378+
parallelLimit: 1 //parallel tasks limit
379+
})
380+
381+
//diff-add
382+
backgroundJobsPlugin.registerTaskDetailsComponent({
383+
//diff-add
384+
jobHandlerName: 'example_job_handler', // Handler name
385+
//diff-add
386+
component: {
387+
//diff-add
388+
file: '@@/JobCustomComponent.vue' //custom component for the job details
389+
//diff-add
390+
},
391+
//diff-add
392+
})
393+
394+
395+
396+
```

0 commit comments

Comments
 (0)