@open-mercato/core 0.4.5-develop-995ce486fa → 0.4.5-develop-8a56591995
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.
- package/dist/generated/entities/progress_job/index.js +57 -0
- package/dist/generated/entities/progress_job/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +4 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/progress/__integration__/TC-PROG-001.spec.js +51 -0
- package/dist/modules/progress/__integration__/TC-PROG-001.spec.js.map +7 -0
- package/dist/modules/progress/acl.js +33 -0
- package/dist/modules/progress/acl.js.map +7 -0
- package/dist/modules/progress/api/active/route.js +57 -0
- package/dist/modules/progress/api/active/route.js.map +7 -0
- package/dist/modules/progress/api/jobs/[id]/route.js +126 -0
- package/dist/modules/progress/api/jobs/[id]/route.js.map +7 -0
- package/dist/modules/progress/api/jobs/route.js +156 -0
- package/dist/modules/progress/api/jobs/route.js.map +7 -0
- package/dist/modules/progress/api/openapi.js +27 -0
- package/dist/modules/progress/api/openapi.js.map +7 -0
- package/dist/modules/progress/data/entities.js +113 -0
- package/dist/modules/progress/data/entities.js.map +7 -0
- package/dist/modules/progress/data/validators.js +48 -0
- package/dist/modules/progress/data/validators.js.map +7 -0
- package/dist/modules/progress/di.js +16 -0
- package/dist/modules/progress/di.js.map +7 -0
- package/dist/modules/progress/events.js +22 -0
- package/dist/modules/progress/events.js.map +7 -0
- package/dist/modules/progress/index.js +13 -0
- package/dist/modules/progress/index.js.map +7 -0
- package/dist/modules/progress/lib/events.js +14 -0
- package/dist/modules/progress/lib/events.js.map +7 -0
- package/dist/modules/progress/lib/progressService.js +21 -0
- package/dist/modules/progress/lib/progressService.js.map +7 -0
- package/dist/modules/progress/lib/progressServiceImpl.js +215 -0
- package/dist/modules/progress/lib/progressServiceImpl.js.map +7 -0
- package/dist/modules/progress/migrations/Migration20260220214819.js +16 -0
- package/dist/modules/progress/migrations/Migration20260220214819.js.map +7 -0
- package/dist/modules/progress/setup.js +12 -0
- package/dist/modules/progress/setup.js.map +7 -0
- package/generated/entities/progress_job/index.ts +27 -0
- package/generated/entities.ids.generated.ts +4 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +2 -2
- package/src/modules/progress/__integration__/TC-PROG-001.spec.ts +67 -0
- package/src/modules/progress/__tests__/progressService.test.ts +377 -0
- package/src/modules/progress/acl.ts +29 -0
- package/src/modules/progress/api/active/route.ts +61 -0
- package/src/modules/progress/api/jobs/[id]/route.ts +136 -0
- package/src/modules/progress/api/jobs/route.ts +192 -0
- package/src/modules/progress/api/openapi.ts +28 -0
- package/src/modules/progress/data/entities.ts +94 -0
- package/src/modules/progress/data/validators.ts +51 -0
- package/src/modules/progress/di.ts +15 -0
- package/src/modules/progress/events.ts +21 -0
- package/src/modules/progress/i18n/de.json +20 -0
- package/src/modules/progress/i18n/en.json +20 -0
- package/src/modules/progress/i18n/es.json +20 -0
- package/src/modules/progress/i18n/pl.json +20 -0
- package/src/modules/progress/index.ts +11 -0
- package/src/modules/progress/lib/events.ts +60 -0
- package/src/modules/progress/lib/progressService.ts +47 -0
- package/src/modules/progress/lib/progressServiceImpl.ts +261 -0
- package/src/modules/progress/migrations/.snapshot-open-mercato.json +316 -0
- package/src/modules/progress/migrations/Migration20260220214819.ts +15 -0
- package/src/modules/progress/setup.ts +10 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { ProgressJob } from "../data/entities.js";
|
|
2
|
+
import { calculateEta, calculateProgressPercent, STALE_JOB_TIMEOUT_SECONDS } from "./progressService.js";
|
|
3
|
+
import { PROGRESS_EVENTS } from "./events.js";
|
|
4
|
+
function createProgressService(em, eventBus) {
|
|
5
|
+
return {
|
|
6
|
+
async createJob(input, ctx) {
|
|
7
|
+
const job = em.create(ProgressJob, {
|
|
8
|
+
jobType: input.jobType,
|
|
9
|
+
name: input.name,
|
|
10
|
+
description: input.description,
|
|
11
|
+
totalCount: input.totalCount,
|
|
12
|
+
cancellable: input.cancellable ?? false,
|
|
13
|
+
meta: input.meta,
|
|
14
|
+
parentJobId: input.parentJobId,
|
|
15
|
+
partitionIndex: input.partitionIndex,
|
|
16
|
+
partitionCount: input.partitionCount,
|
|
17
|
+
startedByUserId: ctx.userId,
|
|
18
|
+
tenantId: ctx.tenantId,
|
|
19
|
+
organizationId: ctx.organizationId,
|
|
20
|
+
status: "pending"
|
|
21
|
+
});
|
|
22
|
+
await em.persistAndFlush(job);
|
|
23
|
+
await eventBus.emit(PROGRESS_EVENTS.JOB_CREATED, {
|
|
24
|
+
jobId: job.id,
|
|
25
|
+
jobType: job.jobType,
|
|
26
|
+
name: job.name,
|
|
27
|
+
tenantId: ctx.tenantId,
|
|
28
|
+
organizationId: ctx.organizationId
|
|
29
|
+
});
|
|
30
|
+
return job;
|
|
31
|
+
},
|
|
32
|
+
async startJob(jobId, ctx) {
|
|
33
|
+
const job = await em.findOneOrFail(ProgressJob, { id: jobId, tenantId: ctx.tenantId });
|
|
34
|
+
job.status = "running";
|
|
35
|
+
job.startedAt = /* @__PURE__ */ new Date();
|
|
36
|
+
job.heartbeatAt = /* @__PURE__ */ new Date();
|
|
37
|
+
await em.flush();
|
|
38
|
+
await eventBus.emit(PROGRESS_EVENTS.JOB_STARTED, {
|
|
39
|
+
jobId: job.id,
|
|
40
|
+
jobType: job.jobType,
|
|
41
|
+
tenantId: ctx.tenantId
|
|
42
|
+
});
|
|
43
|
+
return job;
|
|
44
|
+
},
|
|
45
|
+
async updateProgress(jobId, input, ctx) {
|
|
46
|
+
const job = await em.findOneOrFail(ProgressJob, { id: jobId, tenantId: ctx.tenantId });
|
|
47
|
+
if (input.processedCount !== void 0) {
|
|
48
|
+
job.processedCount = input.processedCount;
|
|
49
|
+
}
|
|
50
|
+
if (input.totalCount !== void 0) {
|
|
51
|
+
job.totalCount = input.totalCount;
|
|
52
|
+
}
|
|
53
|
+
if (input.meta !== void 0) {
|
|
54
|
+
job.meta = { ...job.meta, ...input.meta };
|
|
55
|
+
}
|
|
56
|
+
if (input.progressPercent !== void 0) {
|
|
57
|
+
job.progressPercent = input.progressPercent;
|
|
58
|
+
} else if (job.totalCount) {
|
|
59
|
+
job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount);
|
|
60
|
+
}
|
|
61
|
+
if (input.etaSeconds !== void 0) {
|
|
62
|
+
job.etaSeconds = input.etaSeconds;
|
|
63
|
+
} else if (job.startedAt && job.totalCount) {
|
|
64
|
+
job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt);
|
|
65
|
+
}
|
|
66
|
+
job.heartbeatAt = /* @__PURE__ */ new Date();
|
|
67
|
+
await em.flush();
|
|
68
|
+
await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {
|
|
69
|
+
jobId: job.id,
|
|
70
|
+
jobType: job.jobType,
|
|
71
|
+
progressPercent: job.progressPercent,
|
|
72
|
+
processedCount: job.processedCount,
|
|
73
|
+
totalCount: job.totalCount,
|
|
74
|
+
etaSeconds: job.etaSeconds,
|
|
75
|
+
tenantId: ctx.tenantId
|
|
76
|
+
});
|
|
77
|
+
return job;
|
|
78
|
+
},
|
|
79
|
+
async incrementProgress(jobId, delta, ctx) {
|
|
80
|
+
const job = await em.findOneOrFail(ProgressJob, { id: jobId, tenantId: ctx.tenantId });
|
|
81
|
+
job.processedCount += delta;
|
|
82
|
+
job.heartbeatAt = /* @__PURE__ */ new Date();
|
|
83
|
+
if (job.totalCount) {
|
|
84
|
+
job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount);
|
|
85
|
+
if (job.startedAt) {
|
|
86
|
+
job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
await em.flush();
|
|
90
|
+
await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {
|
|
91
|
+
jobId: job.id,
|
|
92
|
+
progressPercent: job.progressPercent,
|
|
93
|
+
processedCount: job.processedCount,
|
|
94
|
+
tenantId: ctx.tenantId
|
|
95
|
+
});
|
|
96
|
+
return job;
|
|
97
|
+
},
|
|
98
|
+
async completeJob(jobId, input, ctx) {
|
|
99
|
+
const job = await em.findOne(ProgressJob, { id: jobId, tenantId: ctx.tenantId });
|
|
100
|
+
if (!job) throw new Error(`Job ${jobId} not found`);
|
|
101
|
+
job.status = "completed";
|
|
102
|
+
job.finishedAt = /* @__PURE__ */ new Date();
|
|
103
|
+
job.progressPercent = 100;
|
|
104
|
+
job.etaSeconds = 0;
|
|
105
|
+
if (input?.resultSummary) {
|
|
106
|
+
job.resultSummary = input.resultSummary;
|
|
107
|
+
}
|
|
108
|
+
await em.flush();
|
|
109
|
+
await eventBus.emit(PROGRESS_EVENTS.JOB_COMPLETED, {
|
|
110
|
+
jobId: job.id,
|
|
111
|
+
jobType: job.jobType,
|
|
112
|
+
resultSummary: job.resultSummary,
|
|
113
|
+
tenantId: ctx.tenantId
|
|
114
|
+
});
|
|
115
|
+
return job;
|
|
116
|
+
},
|
|
117
|
+
async failJob(jobId, input, ctx) {
|
|
118
|
+
const job = await em.findOne(ProgressJob, { id: jobId, tenantId: ctx.tenantId });
|
|
119
|
+
if (!job) throw new Error(`Job ${jobId} not found`);
|
|
120
|
+
job.status = "failed";
|
|
121
|
+
job.finishedAt = /* @__PURE__ */ new Date();
|
|
122
|
+
job.errorMessage = input.errorMessage;
|
|
123
|
+
job.errorStack = input.errorStack;
|
|
124
|
+
await em.flush();
|
|
125
|
+
await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {
|
|
126
|
+
jobId: job.id,
|
|
127
|
+
jobType: job.jobType,
|
|
128
|
+
errorMessage: job.errorMessage,
|
|
129
|
+
tenantId: ctx.tenantId
|
|
130
|
+
});
|
|
131
|
+
return job;
|
|
132
|
+
},
|
|
133
|
+
async cancelJob(jobId, ctx) {
|
|
134
|
+
const job = await em.findOneOrFail(ProgressJob, {
|
|
135
|
+
id: jobId,
|
|
136
|
+
tenantId: ctx.tenantId,
|
|
137
|
+
cancellable: true,
|
|
138
|
+
status: { $in: ["pending", "running"] }
|
|
139
|
+
});
|
|
140
|
+
job.cancelRequestedAt = /* @__PURE__ */ new Date();
|
|
141
|
+
job.cancelledByUserId = ctx.userId;
|
|
142
|
+
if (job.status === "pending") {
|
|
143
|
+
job.status = "cancelled";
|
|
144
|
+
job.finishedAt = /* @__PURE__ */ new Date();
|
|
145
|
+
}
|
|
146
|
+
await em.flush();
|
|
147
|
+
await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {
|
|
148
|
+
jobId: job.id,
|
|
149
|
+
jobType: job.jobType,
|
|
150
|
+
tenantId: ctx.tenantId
|
|
151
|
+
});
|
|
152
|
+
return job;
|
|
153
|
+
},
|
|
154
|
+
async isCancellationRequested(jobId) {
|
|
155
|
+
const job = await em.findOne(ProgressJob, { id: jobId });
|
|
156
|
+
return job?.cancelRequestedAt != null;
|
|
157
|
+
},
|
|
158
|
+
async getActiveJobs(ctx) {
|
|
159
|
+
return em.find(ProgressJob, {
|
|
160
|
+
tenantId: ctx.tenantId,
|
|
161
|
+
...ctx.organizationId ? { organizationId: ctx.organizationId } : {},
|
|
162
|
+
status: { $in: ["pending", "running"] },
|
|
163
|
+
parentJobId: null
|
|
164
|
+
}, {
|
|
165
|
+
orderBy: { createdAt: "DESC" },
|
|
166
|
+
limit: 50
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
async getRecentlyCompletedJobs(ctx, sinceSeconds = 30) {
|
|
170
|
+
const cutoff = new Date(Date.now() - sinceSeconds * 1e3);
|
|
171
|
+
return em.find(ProgressJob, {
|
|
172
|
+
tenantId: ctx.tenantId,
|
|
173
|
+
...ctx.organizationId ? { organizationId: ctx.organizationId } : {},
|
|
174
|
+
status: { $in: ["completed", "failed"] },
|
|
175
|
+
finishedAt: { $gte: cutoff },
|
|
176
|
+
parentJobId: null
|
|
177
|
+
}, {
|
|
178
|
+
orderBy: { finishedAt: "DESC" },
|
|
179
|
+
limit: 10
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
async getJob(jobId, ctx) {
|
|
183
|
+
return em.findOne(ProgressJob, {
|
|
184
|
+
id: jobId,
|
|
185
|
+
tenantId: ctx.tenantId
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
async markStaleJobsFailed(tenantId, timeoutSeconds = STALE_JOB_TIMEOUT_SECONDS) {
|
|
189
|
+
const cutoff = new Date(Date.now() - timeoutSeconds * 1e3);
|
|
190
|
+
const staleJobs = await em.find(ProgressJob, {
|
|
191
|
+
tenantId,
|
|
192
|
+
status: "running",
|
|
193
|
+
heartbeatAt: { $lt: cutoff }
|
|
194
|
+
});
|
|
195
|
+
for (const job of staleJobs) {
|
|
196
|
+
job.status = "failed";
|
|
197
|
+
job.finishedAt = /* @__PURE__ */ new Date();
|
|
198
|
+
job.errorMessage = `Job stale: no heartbeat for ${timeoutSeconds} seconds`;
|
|
199
|
+
await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {
|
|
200
|
+
jobId: job.id,
|
|
201
|
+
jobType: job.jobType,
|
|
202
|
+
errorMessage: job.errorMessage,
|
|
203
|
+
tenantId: job.tenantId,
|
|
204
|
+
stale: true
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
await em.flush();
|
|
208
|
+
return staleJobs.length;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
export {
|
|
213
|
+
createProgressService
|
|
214
|
+
};
|
|
215
|
+
//# sourceMappingURL=progressServiceImpl.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/progress/lib/progressServiceImpl.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { ProgressJob } from '../data/entities'\nimport type { ProgressService } from './progressService'\nimport { calculateEta, calculateProgressPercent, STALE_JOB_TIMEOUT_SECONDS } from './progressService'\nimport { PROGRESS_EVENTS } from './events'\n\nexport function createProgressService(em: EntityManager, eventBus: { emit: (event: string, payload: Record<string, unknown>) => Promise<void> }): ProgressService {\n return {\n async createJob(input, ctx) {\n const job = em.create(ProgressJob, {\n jobType: input.jobType,\n name: input.name,\n description: input.description,\n totalCount: input.totalCount,\n cancellable: input.cancellable ?? false,\n meta: input.meta,\n parentJobId: input.parentJobId,\n partitionIndex: input.partitionIndex,\n partitionCount: input.partitionCount,\n startedByUserId: ctx.userId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n status: 'pending',\n })\n\n await em.persistAndFlush(job)\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CREATED, {\n jobId: job.id,\n jobType: job.jobType,\n name: job.name,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n\n return job\n },\n\n async startJob(jobId, ctx) {\n const job = await em.findOneOrFail(ProgressJob, { id: jobId, tenantId: ctx.tenantId })\n\n job.status = 'running'\n job.startedAt = new Date()\n job.heartbeatAt = new Date()\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_STARTED, {\n jobId: job.id,\n jobType: job.jobType,\n tenantId: ctx.tenantId,\n })\n\n return job\n },\n\n async updateProgress(jobId, input, ctx) {\n const job = await em.findOneOrFail(ProgressJob, { id: jobId, tenantId: ctx.tenantId })\n\n if (input.processedCount !== undefined) {\n job.processedCount = input.processedCount\n }\n if (input.totalCount !== undefined) {\n job.totalCount = input.totalCount\n }\n if (input.meta !== undefined) {\n job.meta = { ...job.meta, ...input.meta }\n }\n\n if (input.progressPercent !== undefined) {\n job.progressPercent = input.progressPercent\n } else if (job.totalCount) {\n job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount)\n }\n\n if (input.etaSeconds !== undefined) {\n job.etaSeconds = input.etaSeconds\n } else if (job.startedAt && job.totalCount) {\n job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt)\n }\n\n job.heartbeatAt = new Date()\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {\n jobId: job.id,\n jobType: job.jobType,\n progressPercent: job.progressPercent,\n processedCount: job.processedCount,\n totalCount: job.totalCount,\n etaSeconds: job.etaSeconds,\n tenantId: ctx.tenantId,\n })\n\n return job\n },\n\n async incrementProgress(jobId, delta, ctx) {\n const job = await em.findOneOrFail(ProgressJob, { id: jobId, tenantId: ctx.tenantId })\n\n job.processedCount += delta\n job.heartbeatAt = new Date()\n\n if (job.totalCount) {\n job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount)\n if (job.startedAt) {\n job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt)\n }\n }\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {\n jobId: job.id,\n progressPercent: job.progressPercent,\n processedCount: job.processedCount,\n tenantId: ctx.tenantId,\n })\n\n return job\n },\n\n async completeJob(jobId, input, ctx) {\n const job = await em.findOne(ProgressJob, { id: jobId, tenantId: ctx.tenantId })\n if (!job) throw new Error(`Job ${jobId} not found`)\n\n job.status = 'completed'\n job.finishedAt = new Date()\n job.progressPercent = 100\n job.etaSeconds = 0\n if (input?.resultSummary) {\n job.resultSummary = input.resultSummary\n }\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_COMPLETED, {\n jobId: job.id,\n jobType: job.jobType,\n resultSummary: job.resultSummary,\n tenantId: ctx.tenantId,\n })\n\n return job\n },\n\n async failJob(jobId, input, ctx) {\n const job = await em.findOne(ProgressJob, { id: jobId, tenantId: ctx.tenantId })\n if (!job) throw new Error(`Job ${jobId} not found`)\n\n job.status = 'failed'\n job.finishedAt = new Date()\n job.errorMessage = input.errorMessage\n job.errorStack = input.errorStack\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n jobId: job.id,\n jobType: job.jobType,\n errorMessage: job.errorMessage,\n tenantId: ctx.tenantId,\n })\n\n return job\n },\n\n async cancelJob(jobId, ctx) {\n const job = await em.findOneOrFail(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n cancellable: true,\n status: { $in: ['pending', 'running'] },\n })\n\n job.cancelRequestedAt = new Date()\n job.cancelledByUserId = ctx.userId\n\n if (job.status === 'pending') {\n job.status = 'cancelled'\n job.finishedAt = new Date()\n }\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {\n jobId: job.id,\n jobType: job.jobType,\n tenantId: ctx.tenantId,\n })\n\n return job\n },\n\n async isCancellationRequested(jobId) {\n const job = await em.findOne(ProgressJob, { id: jobId })\n return job?.cancelRequestedAt != null\n },\n\n async getActiveJobs(ctx) {\n return em.find(ProgressJob, {\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n status: { $in: ['pending', 'running'] },\n parentJobId: null,\n }, {\n orderBy: { createdAt: 'DESC' },\n limit: 50,\n })\n },\n\n async getRecentlyCompletedJobs(ctx, sinceSeconds = 30) {\n const cutoff = new Date(Date.now() - sinceSeconds * 1000)\n return em.find(ProgressJob, {\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n status: { $in: ['completed', 'failed'] },\n finishedAt: { $gte: cutoff },\n parentJobId: null,\n }, {\n orderBy: { finishedAt: 'DESC' },\n limit: 10,\n })\n },\n\n async getJob(jobId, ctx) {\n return em.findOne(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n })\n },\n\n async markStaleJobsFailed(tenantId: string, timeoutSeconds = STALE_JOB_TIMEOUT_SECONDS) {\n const cutoff = new Date(Date.now() - timeoutSeconds * 1000)\n\n const staleJobs = await em.find(ProgressJob, {\n tenantId,\n status: 'running',\n heartbeatAt: { $lt: cutoff },\n })\n\n for (const job of staleJobs) {\n job.status = 'failed'\n job.finishedAt = new Date()\n job.errorMessage = `Job stale: no heartbeat for ${timeoutSeconds} seconds`\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n jobId: job.id,\n jobType: job.jobType,\n errorMessage: job.errorMessage,\n tenantId: job.tenantId,\n stale: true,\n })\n }\n\n await em.flush()\n return staleJobs.length\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
class Migration20260220214819 extends Migration {
|
|
3
|
+
async up() {
|
|
4
|
+
this.addSql(`create table "progress_jobs" ("id" uuid not null default gen_random_uuid(), "job_type" text not null, "name" text not null, "description" text null, "status" text not null default 'pending', "progress_percent" smallint not null default 0, "processed_count" int not null default 0, "total_count" int null, "eta_seconds" int null, "started_by_user_id" uuid null, "started_at" timestamptz null, "heartbeat_at" timestamptz null, "finished_at" timestamptz null, "result_summary" jsonb null, "error_message" text null, "error_stack" text null, "meta" jsonb null, "cancellable" boolean not null default false, "cancelled_by_user_id" uuid null, "cancel_requested_at" timestamptz null, "parent_job_id" uuid null, "partition_index" int null, "partition_count" int null, "tenant_id" uuid not null, "organization_id" uuid null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "progress_jobs_pkey" primary key ("id"));`);
|
|
5
|
+
this.addSql(`create index "progress_jobs_parent_idx" on "progress_jobs" ("parent_job_id");`);
|
|
6
|
+
this.addSql(`create index "progress_jobs_type_tenant_idx" on "progress_jobs" ("job_type", "tenant_id");`);
|
|
7
|
+
this.addSql(`create index "progress_jobs_status_tenant_idx" on "progress_jobs" ("status", "tenant_id");`);
|
|
8
|
+
}
|
|
9
|
+
async down() {
|
|
10
|
+
this.addSql(`drop table if exists "progress_jobs";`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
Migration20260220214819
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=Migration20260220214819.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/progress/migrations/Migration20260220214819.ts"],
|
|
4
|
+
"sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20260220214819 extends Migration {\n\n override async up(): Promise<void> {\n this.addSql(`create table \"progress_jobs\" (\"id\" uuid not null default gen_random_uuid(), \"job_type\" text not null, \"name\" text not null, \"description\" text null, \"status\" text not null default 'pending', \"progress_percent\" smallint not null default 0, \"processed_count\" int not null default 0, \"total_count\" int null, \"eta_seconds\" int null, \"started_by_user_id\" uuid null, \"started_at\" timestamptz null, \"heartbeat_at\" timestamptz null, \"finished_at\" timestamptz null, \"result_summary\" jsonb null, \"error_message\" text null, \"error_stack\" text null, \"meta\" jsonb null, \"cancellable\" boolean not null default false, \"cancelled_by_user_id\" uuid null, \"cancel_requested_at\" timestamptz null, \"parent_job_id\" uuid null, \"partition_index\" int null, \"partition_count\" int null, \"tenant_id\" uuid not null, \"organization_id\" uuid null, \"created_at\" timestamptz not null, \"updated_at\" timestamptz not null, constraint \"progress_jobs_pkey\" primary key (\"id\"));`);\n this.addSql(`create index \"progress_jobs_parent_idx\" on \"progress_jobs\" (\"parent_job_id\");`);\n this.addSql(`create index \"progress_jobs_type_tenant_idx\" on \"progress_jobs\" (\"job_type\", \"tenant_id\");`);\n this.addSql(`create index \"progress_jobs_status_tenant_idx\" on \"progress_jobs\" (\"status\", \"tenant_id\");`);\n }\n\n override async down(): Promise<void> {\n this.addSql(`drop table if exists \"progress_jobs\";`);\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,gCAAgC,UAAU;AAAA,EAErD,MAAe,KAAoB;AACjC,SAAK,OAAO,46BAA46B;AACx7B,SAAK,OAAO,+EAA+E;AAC3F,SAAK,OAAO,4FAA4F;AACxG,SAAK,OAAO,4FAA4F;AAAA,EAC1G;AAAA,EAEA,MAAe,OAAsB;AACnC,SAAK,OAAO,uCAAuC;AAAA,EACrD;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/progress/setup.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: ['progress.*'],\n employee: ['progress.view'],\n },\n}\n\nexport default setup\n"],
|
|
5
|
+
"mappings": "AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,OAAO,CAAC,YAAY;AAAA,IACpB,UAAU,CAAC,eAAe;AAAA,EAC5B;AACF;AAEA,IAAO,gBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const id = 'id'
|
|
2
|
+
export const job_type = 'job_type'
|
|
3
|
+
export const name = 'name'
|
|
4
|
+
export const description = 'description'
|
|
5
|
+
export const status = 'status'
|
|
6
|
+
export const progress_percent = 'progress_percent'
|
|
7
|
+
export const processed_count = 'processed_count'
|
|
8
|
+
export const total_count = 'total_count'
|
|
9
|
+
export const eta_seconds = 'eta_seconds'
|
|
10
|
+
export const started_by_user_id = 'started_by_user_id'
|
|
11
|
+
export const started_at = 'started_at'
|
|
12
|
+
export const heartbeat_at = 'heartbeat_at'
|
|
13
|
+
export const finished_at = 'finished_at'
|
|
14
|
+
export const result_summary = 'result_summary'
|
|
15
|
+
export const error_message = 'error_message'
|
|
16
|
+
export const error_stack = 'error_stack'
|
|
17
|
+
export const meta = 'meta'
|
|
18
|
+
export const cancellable = 'cancellable'
|
|
19
|
+
export const cancelled_by_user_id = 'cancelled_by_user_id'
|
|
20
|
+
export const cancel_requested_at = 'cancel_requested_at'
|
|
21
|
+
export const parent_job_id = 'parent_job_id'
|
|
22
|
+
export const partition_index = 'partition_index'
|
|
23
|
+
export const partition_count = 'partition_count'
|
|
24
|
+
export const tenant_id = 'tenant_id'
|
|
25
|
+
export const organization_id = 'organization_id'
|
|
26
|
+
export const created_at = 'created_at'
|
|
27
|
+
export const updated_at = 'updated_at'
|
|
@@ -23,6 +23,7 @@ export const M = {
|
|
|
23
23
|
"resources": "resources",
|
|
24
24
|
"staff": "staff",
|
|
25
25
|
"notifications": "notifications",
|
|
26
|
+
"progress": "progress",
|
|
26
27
|
"translations": "translations",
|
|
27
28
|
"inbox_ops": "inbox_ops"
|
|
28
29
|
} as const
|
|
@@ -190,6 +191,9 @@ export const E = {
|
|
|
190
191
|
"notifications": {
|
|
191
192
|
"notification": "notifications:notification"
|
|
192
193
|
},
|
|
194
|
+
"progress": {
|
|
195
|
+
"progress_job": "progress:progress_job"
|
|
196
|
+
},
|
|
193
197
|
"translations": {
|
|
194
198
|
"entity_translation": "translations:entity_translation"
|
|
195
199
|
},
|
|
@@ -65,6 +65,7 @@ import * as password_reset from './entities/password_reset/index'
|
|
|
65
65
|
import * as perspective from './entities/perspective/index'
|
|
66
66
|
import * as planner_availability_rule from './entities/planner_availability_rule/index'
|
|
67
67
|
import * as planner_availability_rule_set from './entities/planner_availability_rule_set/index'
|
|
68
|
+
import * as progress_job from './entities/progress_job/index'
|
|
68
69
|
import * as resources_resource from './entities/resources_resource/index'
|
|
69
70
|
import * as resources_resource_activity from './entities/resources_resource_activity/index'
|
|
70
71
|
import * as resources_resource_comment from './entities/resources_resource_comment/index'
|
|
@@ -192,6 +193,7 @@ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
|
|
|
192
193
|
perspective,
|
|
193
194
|
planner_availability_rule,
|
|
194
195
|
planner_availability_rule_set,
|
|
196
|
+
progress_job,
|
|
195
197
|
resources_resource,
|
|
196
198
|
resources_resource_activity,
|
|
197
199
|
resources_resource_comment,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.4.5-develop-
|
|
3
|
+
"version": "0.4.5-develop-8a56591995",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
}
|
|
208
208
|
},
|
|
209
209
|
"dependencies": {
|
|
210
|
-
"@open-mercato/shared": "0.4.5-develop-
|
|
210
|
+
"@open-mercato/shared": "0.4.5-develop-8a56591995",
|
|
211
211
|
"@types/semver": "^7.5.8",
|
|
212
212
|
"@xyflow/react": "^12.6.0",
|
|
213
213
|
"ai": "^6.0.0",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { getAuthToken, apiRequest } from '@open-mercato/core/modules/core/__integration__/helpers/api';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TC-PROG-001: Progress Job Lifecycle
|
|
6
|
+
* Covers: create, list active, update progress, cancel
|
|
7
|
+
*/
|
|
8
|
+
test.describe('TC-PROG-001: Progress Job Lifecycle', () => {
|
|
9
|
+
test('should create, list, update, and cancel a progress job', async ({ request }) => {
|
|
10
|
+
let token: string | null = null;
|
|
11
|
+
let jobId: string | null = null;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
token = await getAuthToken(request);
|
|
15
|
+
|
|
16
|
+
// 1. Create a job via POST /api/progress/jobs
|
|
17
|
+
const createRes = await apiRequest(request, 'POST', '/api/progress/jobs', {
|
|
18
|
+
token,
|
|
19
|
+
data: {
|
|
20
|
+
jobType: 'integration-test',
|
|
21
|
+
name: `QA TC-PROG-001 ${Date.now()}`,
|
|
22
|
+
totalCount: 200,
|
|
23
|
+
cancellable: true,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
expect(createRes.status()).toBe(201);
|
|
27
|
+
const createBody = await createRes.json();
|
|
28
|
+
expect(createBody.id).toBeTruthy();
|
|
29
|
+
jobId = createBody.id;
|
|
30
|
+
|
|
31
|
+
// 2. Verify it appears in GET /api/progress/active
|
|
32
|
+
const activeRes = await apiRequest(request, 'GET', '/api/progress/active', { token });
|
|
33
|
+
expect(activeRes.ok()).toBeTruthy();
|
|
34
|
+
const activeBody = await activeRes.json();
|
|
35
|
+
const activeIds = activeBody.active.map((j: { id: string }) => j.id);
|
|
36
|
+
expect(activeIds).toContain(jobId);
|
|
37
|
+
|
|
38
|
+
// 3. Update progress via PUT /api/progress/jobs/:id
|
|
39
|
+
const updateRes = await apiRequest(request, 'PUT', `/api/progress/jobs/${jobId}`, {
|
|
40
|
+
token,
|
|
41
|
+
data: { processedCount: 80, totalCount: 200 },
|
|
42
|
+
});
|
|
43
|
+
expect(updateRes.ok()).toBeTruthy();
|
|
44
|
+
const updateBody = await updateRes.json();
|
|
45
|
+
expect(updateBody.ok).toBe(true);
|
|
46
|
+
expect(updateBody.progressPercent).toBe(40);
|
|
47
|
+
|
|
48
|
+
// 4. Cancel job via DELETE /api/progress/jobs/:id
|
|
49
|
+
const cancelRes = await apiRequest(request, 'DELETE', `/api/progress/jobs/${jobId}`, { token });
|
|
50
|
+
expect(cancelRes.ok()).toBeTruthy();
|
|
51
|
+
const cancelBody = await cancelRes.json();
|
|
52
|
+
expect(cancelBody.ok).toBe(true);
|
|
53
|
+
|
|
54
|
+
// 5. Verify the job is now cancelled (pending jobs cancel immediately)
|
|
55
|
+
const detailRes = await apiRequest(request, 'GET', `/api/progress/jobs/${jobId}`, { token });
|
|
56
|
+
expect(detailRes.ok()).toBeTruthy();
|
|
57
|
+
const detailBody = await detailRes.json();
|
|
58
|
+
expect(detailBody.status).toBe('cancelled');
|
|
59
|
+
} finally {
|
|
60
|
+
// Jobs are soft state — no explicit cleanup needed.
|
|
61
|
+
// If the test created a job that wasn't cancelled, cancel it to keep state clean.
|
|
62
|
+
if (token && jobId) {
|
|
63
|
+
await apiRequest(request, 'DELETE', `/api/progress/jobs/${jobId}`, { token }).catch(() => {});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|