@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.
Files changed (64) hide show
  1. package/dist/generated/entities/progress_job/index.js +57 -0
  2. package/dist/generated/entities/progress_job/index.js.map +7 -0
  3. package/dist/generated/entities.ids.generated.js +4 -0
  4. package/dist/generated/entities.ids.generated.js.map +2 -2
  5. package/dist/generated/entity-fields-registry.js +2 -0
  6. package/dist/generated/entity-fields-registry.js.map +2 -2
  7. package/dist/modules/progress/__integration__/TC-PROG-001.spec.js +51 -0
  8. package/dist/modules/progress/__integration__/TC-PROG-001.spec.js.map +7 -0
  9. package/dist/modules/progress/acl.js +33 -0
  10. package/dist/modules/progress/acl.js.map +7 -0
  11. package/dist/modules/progress/api/active/route.js +57 -0
  12. package/dist/modules/progress/api/active/route.js.map +7 -0
  13. package/dist/modules/progress/api/jobs/[id]/route.js +126 -0
  14. package/dist/modules/progress/api/jobs/[id]/route.js.map +7 -0
  15. package/dist/modules/progress/api/jobs/route.js +156 -0
  16. package/dist/modules/progress/api/jobs/route.js.map +7 -0
  17. package/dist/modules/progress/api/openapi.js +27 -0
  18. package/dist/modules/progress/api/openapi.js.map +7 -0
  19. package/dist/modules/progress/data/entities.js +113 -0
  20. package/dist/modules/progress/data/entities.js.map +7 -0
  21. package/dist/modules/progress/data/validators.js +48 -0
  22. package/dist/modules/progress/data/validators.js.map +7 -0
  23. package/dist/modules/progress/di.js +16 -0
  24. package/dist/modules/progress/di.js.map +7 -0
  25. package/dist/modules/progress/events.js +22 -0
  26. package/dist/modules/progress/events.js.map +7 -0
  27. package/dist/modules/progress/index.js +13 -0
  28. package/dist/modules/progress/index.js.map +7 -0
  29. package/dist/modules/progress/lib/events.js +14 -0
  30. package/dist/modules/progress/lib/events.js.map +7 -0
  31. package/dist/modules/progress/lib/progressService.js +21 -0
  32. package/dist/modules/progress/lib/progressService.js.map +7 -0
  33. package/dist/modules/progress/lib/progressServiceImpl.js +215 -0
  34. package/dist/modules/progress/lib/progressServiceImpl.js.map +7 -0
  35. package/dist/modules/progress/migrations/Migration20260220214819.js +16 -0
  36. package/dist/modules/progress/migrations/Migration20260220214819.js.map +7 -0
  37. package/dist/modules/progress/setup.js +12 -0
  38. package/dist/modules/progress/setup.js.map +7 -0
  39. package/generated/entities/progress_job/index.ts +27 -0
  40. package/generated/entities.ids.generated.ts +4 -0
  41. package/generated/entity-fields-registry.ts +2 -0
  42. package/package.json +2 -2
  43. package/src/modules/progress/__integration__/TC-PROG-001.spec.ts +67 -0
  44. package/src/modules/progress/__tests__/progressService.test.ts +377 -0
  45. package/src/modules/progress/acl.ts +29 -0
  46. package/src/modules/progress/api/active/route.ts +61 -0
  47. package/src/modules/progress/api/jobs/[id]/route.ts +136 -0
  48. package/src/modules/progress/api/jobs/route.ts +192 -0
  49. package/src/modules/progress/api/openapi.ts +28 -0
  50. package/src/modules/progress/data/entities.ts +94 -0
  51. package/src/modules/progress/data/validators.ts +51 -0
  52. package/src/modules/progress/di.ts +15 -0
  53. package/src/modules/progress/events.ts +21 -0
  54. package/src/modules/progress/i18n/de.json +20 -0
  55. package/src/modules/progress/i18n/en.json +20 -0
  56. package/src/modules/progress/i18n/es.json +20 -0
  57. package/src/modules/progress/i18n/pl.json +20 -0
  58. package/src/modules/progress/index.ts +11 -0
  59. package/src/modules/progress/lib/events.ts +60 -0
  60. package/src/modules/progress/lib/progressService.ts +47 -0
  61. package/src/modules/progress/lib/progressServiceImpl.ts +261 -0
  62. package/src/modules/progress/migrations/.snapshot-open-mercato.json +316 -0
  63. package/src/modules/progress/migrations/Migration20260220214819.ts +15 -0
  64. 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,12 @@
1
+ const setup = {
2
+ defaultRoleFeatures: {
3
+ admin: ["progress.*"],
4
+ employee: ["progress.view"]
5
+ }
6
+ };
7
+ var setup_default = setup;
8
+ export {
9
+ setup_default as default,
10
+ setup
11
+ };
12
+ //# sourceMappingURL=setup.js.map
@@ -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-995ce486fa",
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-995ce486fa",
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
+ });