@open-mercato/core 0.4.5-develop-995ce486fa → 0.4.5-develop-17cfa89764

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 (65) hide show
  1. package/AGENTS.md +2 -1
  2. package/dist/generated/entities/progress_job/index.js +57 -0
  3. package/dist/generated/entities/progress_job/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +4 -0
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/progress/__integration__/TC-PROG-001.spec.js +51 -0
  9. package/dist/modules/progress/__integration__/TC-PROG-001.spec.js.map +7 -0
  10. package/dist/modules/progress/acl.js +33 -0
  11. package/dist/modules/progress/acl.js.map +7 -0
  12. package/dist/modules/progress/api/active/route.js +57 -0
  13. package/dist/modules/progress/api/active/route.js.map +7 -0
  14. package/dist/modules/progress/api/jobs/[id]/route.js +126 -0
  15. package/dist/modules/progress/api/jobs/[id]/route.js.map +7 -0
  16. package/dist/modules/progress/api/jobs/route.js +156 -0
  17. package/dist/modules/progress/api/jobs/route.js.map +7 -0
  18. package/dist/modules/progress/api/openapi.js +27 -0
  19. package/dist/modules/progress/api/openapi.js.map +7 -0
  20. package/dist/modules/progress/data/entities.js +113 -0
  21. package/dist/modules/progress/data/entities.js.map +7 -0
  22. package/dist/modules/progress/data/validators.js +48 -0
  23. package/dist/modules/progress/data/validators.js.map +7 -0
  24. package/dist/modules/progress/di.js +16 -0
  25. package/dist/modules/progress/di.js.map +7 -0
  26. package/dist/modules/progress/events.js +22 -0
  27. package/dist/modules/progress/events.js.map +7 -0
  28. package/dist/modules/progress/index.js +13 -0
  29. package/dist/modules/progress/index.js.map +7 -0
  30. package/dist/modules/progress/lib/events.js +14 -0
  31. package/dist/modules/progress/lib/events.js.map +7 -0
  32. package/dist/modules/progress/lib/progressService.js +21 -0
  33. package/dist/modules/progress/lib/progressService.js.map +7 -0
  34. package/dist/modules/progress/lib/progressServiceImpl.js +215 -0
  35. package/dist/modules/progress/lib/progressServiceImpl.js.map +7 -0
  36. package/dist/modules/progress/migrations/Migration20260220214819.js +16 -0
  37. package/dist/modules/progress/migrations/Migration20260220214819.js.map +7 -0
  38. package/dist/modules/progress/setup.js +12 -0
  39. package/dist/modules/progress/setup.js.map +7 -0
  40. package/generated/entities/progress_job/index.ts +27 -0
  41. package/generated/entities.ids.generated.ts +4 -0
  42. package/generated/entity-fields-registry.ts +2 -0
  43. package/package.json +2 -2
  44. package/src/modules/progress/__integration__/TC-PROG-001.spec.ts +67 -0
  45. package/src/modules/progress/__tests__/progressService.test.ts +377 -0
  46. package/src/modules/progress/acl.ts +29 -0
  47. package/src/modules/progress/api/active/route.ts +61 -0
  48. package/src/modules/progress/api/jobs/[id]/route.ts +136 -0
  49. package/src/modules/progress/api/jobs/route.ts +192 -0
  50. package/src/modules/progress/api/openapi.ts +28 -0
  51. package/src/modules/progress/data/entities.ts +94 -0
  52. package/src/modules/progress/data/validators.ts +51 -0
  53. package/src/modules/progress/di.ts +15 -0
  54. package/src/modules/progress/events.ts +21 -0
  55. package/src/modules/progress/i18n/de.json +20 -0
  56. package/src/modules/progress/i18n/en.json +20 -0
  57. package/src/modules/progress/i18n/es.json +20 -0
  58. package/src/modules/progress/i18n/pl.json +20 -0
  59. package/src/modules/progress/index.ts +11 -0
  60. package/src/modules/progress/lib/events.ts +60 -0
  61. package/src/modules/progress/lib/progressService.ts +47 -0
  62. package/src/modules/progress/lib/progressServiceImpl.ts +261 -0
  63. package/src/modules/progress/migrations/.snapshot-open-mercato.json +316 -0
  64. package/src/modules/progress/migrations/Migration20260220214819.ts +15 -0
  65. package/src/modules/progress/setup.ts +10 -0
@@ -0,0 +1,261 @@
1
+ import type { EntityManager } from '@mikro-orm/core'
2
+ import { ProgressJob } from '../data/entities'
3
+ import type { ProgressService } from './progressService'
4
+ import { calculateEta, calculateProgressPercent, STALE_JOB_TIMEOUT_SECONDS } from './progressService'
5
+ import { PROGRESS_EVENTS } from './events'
6
+
7
+ export function createProgressService(em: EntityManager, eventBus: { emit: (event: string, payload: Record<string, unknown>) => Promise<void> }): ProgressService {
8
+ return {
9
+ async createJob(input, ctx) {
10
+ const job = em.create(ProgressJob, {
11
+ jobType: input.jobType,
12
+ name: input.name,
13
+ description: input.description,
14
+ totalCount: input.totalCount,
15
+ cancellable: input.cancellable ?? false,
16
+ meta: input.meta,
17
+ parentJobId: input.parentJobId,
18
+ partitionIndex: input.partitionIndex,
19
+ partitionCount: input.partitionCount,
20
+ startedByUserId: ctx.userId,
21
+ tenantId: ctx.tenantId,
22
+ organizationId: ctx.organizationId,
23
+ status: 'pending',
24
+ })
25
+
26
+ await em.persistAndFlush(job)
27
+
28
+ await eventBus.emit(PROGRESS_EVENTS.JOB_CREATED, {
29
+ jobId: job.id,
30
+ jobType: job.jobType,
31
+ name: job.name,
32
+ tenantId: ctx.tenantId,
33
+ organizationId: ctx.organizationId,
34
+ })
35
+
36
+ return job
37
+ },
38
+
39
+ async startJob(jobId, ctx) {
40
+ const job = await em.findOneOrFail(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
41
+
42
+ job.status = 'running'
43
+ job.startedAt = new Date()
44
+ job.heartbeatAt = new Date()
45
+
46
+ await em.flush()
47
+
48
+ await eventBus.emit(PROGRESS_EVENTS.JOB_STARTED, {
49
+ jobId: job.id,
50
+ jobType: job.jobType,
51
+ tenantId: ctx.tenantId,
52
+ })
53
+
54
+ return job
55
+ },
56
+
57
+ async updateProgress(jobId, input, ctx) {
58
+ const job = await em.findOneOrFail(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
59
+
60
+ if (input.processedCount !== undefined) {
61
+ job.processedCount = input.processedCount
62
+ }
63
+ if (input.totalCount !== undefined) {
64
+ job.totalCount = input.totalCount
65
+ }
66
+ if (input.meta !== undefined) {
67
+ job.meta = { ...job.meta, ...input.meta }
68
+ }
69
+
70
+ if (input.progressPercent !== undefined) {
71
+ job.progressPercent = input.progressPercent
72
+ } else if (job.totalCount) {
73
+ job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount)
74
+ }
75
+
76
+ if (input.etaSeconds !== undefined) {
77
+ job.etaSeconds = input.etaSeconds
78
+ } else if (job.startedAt && job.totalCount) {
79
+ job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt)
80
+ }
81
+
82
+ job.heartbeatAt = new Date()
83
+
84
+ await em.flush()
85
+
86
+ await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {
87
+ jobId: job.id,
88
+ jobType: job.jobType,
89
+ progressPercent: job.progressPercent,
90
+ processedCount: job.processedCount,
91
+ totalCount: job.totalCount,
92
+ etaSeconds: job.etaSeconds,
93
+ tenantId: ctx.tenantId,
94
+ })
95
+
96
+ return job
97
+ },
98
+
99
+ async incrementProgress(jobId, delta, ctx) {
100
+ const job = await em.findOneOrFail(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
101
+
102
+ job.processedCount += delta
103
+ job.heartbeatAt = new Date()
104
+
105
+ if (job.totalCount) {
106
+ job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount)
107
+ if (job.startedAt) {
108
+ job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt)
109
+ }
110
+ }
111
+
112
+ await em.flush()
113
+
114
+ await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {
115
+ jobId: job.id,
116
+ progressPercent: job.progressPercent,
117
+ processedCount: job.processedCount,
118
+ tenantId: ctx.tenantId,
119
+ })
120
+
121
+ return job
122
+ },
123
+
124
+ async completeJob(jobId, input, ctx) {
125
+ const job = await em.findOne(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
126
+ if (!job) throw new Error(`Job ${jobId} not found`)
127
+
128
+ job.status = 'completed'
129
+ job.finishedAt = new Date()
130
+ job.progressPercent = 100
131
+ job.etaSeconds = 0
132
+ if (input?.resultSummary) {
133
+ job.resultSummary = input.resultSummary
134
+ }
135
+
136
+ await em.flush()
137
+
138
+ await eventBus.emit(PROGRESS_EVENTS.JOB_COMPLETED, {
139
+ jobId: job.id,
140
+ jobType: job.jobType,
141
+ resultSummary: job.resultSummary,
142
+ tenantId: ctx.tenantId,
143
+ })
144
+
145
+ return job
146
+ },
147
+
148
+ async failJob(jobId, input, ctx) {
149
+ const job = await em.findOne(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
150
+ if (!job) throw new Error(`Job ${jobId} not found`)
151
+
152
+ job.status = 'failed'
153
+ job.finishedAt = new Date()
154
+ job.errorMessage = input.errorMessage
155
+ job.errorStack = input.errorStack
156
+
157
+ await em.flush()
158
+
159
+ await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {
160
+ jobId: job.id,
161
+ jobType: job.jobType,
162
+ errorMessage: job.errorMessage,
163
+ tenantId: ctx.tenantId,
164
+ })
165
+
166
+ return job
167
+ },
168
+
169
+ async cancelJob(jobId, ctx) {
170
+ const job = await em.findOneOrFail(ProgressJob, {
171
+ id: jobId,
172
+ tenantId: ctx.tenantId,
173
+ cancellable: true,
174
+ status: { $in: ['pending', 'running'] },
175
+ })
176
+
177
+ job.cancelRequestedAt = new Date()
178
+ job.cancelledByUserId = ctx.userId
179
+
180
+ if (job.status === 'pending') {
181
+ job.status = 'cancelled'
182
+ job.finishedAt = new Date()
183
+ }
184
+
185
+ await em.flush()
186
+
187
+ await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {
188
+ jobId: job.id,
189
+ jobType: job.jobType,
190
+ tenantId: ctx.tenantId,
191
+ })
192
+
193
+ return job
194
+ },
195
+
196
+ async isCancellationRequested(jobId) {
197
+ const job = await em.findOne(ProgressJob, { id: jobId })
198
+ return job?.cancelRequestedAt != null
199
+ },
200
+
201
+ async getActiveJobs(ctx) {
202
+ return em.find(ProgressJob, {
203
+ tenantId: ctx.tenantId,
204
+ ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
205
+ status: { $in: ['pending', 'running'] },
206
+ parentJobId: null,
207
+ }, {
208
+ orderBy: { createdAt: 'DESC' },
209
+ limit: 50,
210
+ })
211
+ },
212
+
213
+ async getRecentlyCompletedJobs(ctx, sinceSeconds = 30) {
214
+ const cutoff = new Date(Date.now() - sinceSeconds * 1000)
215
+ return em.find(ProgressJob, {
216
+ tenantId: ctx.tenantId,
217
+ ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
218
+ status: { $in: ['completed', 'failed'] },
219
+ finishedAt: { $gte: cutoff },
220
+ parentJobId: null,
221
+ }, {
222
+ orderBy: { finishedAt: 'DESC' },
223
+ limit: 10,
224
+ })
225
+ },
226
+
227
+ async getJob(jobId, ctx) {
228
+ return em.findOne(ProgressJob, {
229
+ id: jobId,
230
+ tenantId: ctx.tenantId,
231
+ })
232
+ },
233
+
234
+ async markStaleJobsFailed(tenantId: string, timeoutSeconds = STALE_JOB_TIMEOUT_SECONDS) {
235
+ const cutoff = new Date(Date.now() - timeoutSeconds * 1000)
236
+
237
+ const staleJobs = await em.find(ProgressJob, {
238
+ tenantId,
239
+ status: 'running',
240
+ heartbeatAt: { $lt: cutoff },
241
+ })
242
+
243
+ for (const job of staleJobs) {
244
+ job.status = 'failed'
245
+ job.finishedAt = new Date()
246
+ job.errorMessage = `Job stale: no heartbeat for ${timeoutSeconds} seconds`
247
+
248
+ await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {
249
+ jobId: job.id,
250
+ jobType: job.jobType,
251
+ errorMessage: job.errorMessage,
252
+ tenantId: job.tenantId,
253
+ stale: true,
254
+ })
255
+ }
256
+
257
+ await em.flush()
258
+ return staleJobs.length
259
+ },
260
+ }
261
+ }
@@ -0,0 +1,316 @@
1
+ {
2
+ "namespaces": [
3
+ "public"
4
+ ],
5
+ "name": "public",
6
+ "tables": [
7
+ {
8
+ "columns": {
9
+ "id": {
10
+ "name": "id",
11
+ "type": "uuid",
12
+ "unsigned": false,
13
+ "autoincrement": false,
14
+ "primary": false,
15
+ "nullable": false,
16
+ "default": "gen_random_uuid()",
17
+ "mappedType": "uuid"
18
+ },
19
+ "job_type": {
20
+ "name": "job_type",
21
+ "type": "text",
22
+ "unsigned": false,
23
+ "autoincrement": false,
24
+ "primary": false,
25
+ "nullable": false,
26
+ "mappedType": "text"
27
+ },
28
+ "name": {
29
+ "name": "name",
30
+ "type": "text",
31
+ "unsigned": false,
32
+ "autoincrement": false,
33
+ "primary": false,
34
+ "nullable": false,
35
+ "mappedType": "text"
36
+ },
37
+ "description": {
38
+ "name": "description",
39
+ "type": "text",
40
+ "unsigned": false,
41
+ "autoincrement": false,
42
+ "primary": false,
43
+ "nullable": true,
44
+ "mappedType": "text"
45
+ },
46
+ "status": {
47
+ "name": "status",
48
+ "type": "text",
49
+ "unsigned": false,
50
+ "autoincrement": false,
51
+ "primary": false,
52
+ "nullable": false,
53
+ "default": "'pending'",
54
+ "mappedType": "text"
55
+ },
56
+ "progress_percent": {
57
+ "name": "progress_percent",
58
+ "type": "smallint",
59
+ "unsigned": false,
60
+ "autoincrement": false,
61
+ "primary": false,
62
+ "nullable": false,
63
+ "default": "0",
64
+ "mappedType": "smallint"
65
+ },
66
+ "processed_count": {
67
+ "name": "processed_count",
68
+ "type": "int",
69
+ "unsigned": false,
70
+ "autoincrement": false,
71
+ "primary": false,
72
+ "nullable": false,
73
+ "default": "0",
74
+ "mappedType": "integer"
75
+ },
76
+ "total_count": {
77
+ "name": "total_count",
78
+ "type": "int",
79
+ "unsigned": false,
80
+ "autoincrement": false,
81
+ "primary": false,
82
+ "nullable": true,
83
+ "mappedType": "integer"
84
+ },
85
+ "eta_seconds": {
86
+ "name": "eta_seconds",
87
+ "type": "int",
88
+ "unsigned": false,
89
+ "autoincrement": false,
90
+ "primary": false,
91
+ "nullable": true,
92
+ "mappedType": "integer"
93
+ },
94
+ "started_by_user_id": {
95
+ "name": "started_by_user_id",
96
+ "type": "uuid",
97
+ "unsigned": false,
98
+ "autoincrement": false,
99
+ "primary": false,
100
+ "nullable": true,
101
+ "mappedType": "uuid"
102
+ },
103
+ "started_at": {
104
+ "name": "started_at",
105
+ "type": "timestamptz",
106
+ "unsigned": false,
107
+ "autoincrement": false,
108
+ "primary": false,
109
+ "nullable": true,
110
+ "length": 6,
111
+ "mappedType": "datetime"
112
+ },
113
+ "heartbeat_at": {
114
+ "name": "heartbeat_at",
115
+ "type": "timestamptz",
116
+ "unsigned": false,
117
+ "autoincrement": false,
118
+ "primary": false,
119
+ "nullable": true,
120
+ "length": 6,
121
+ "mappedType": "datetime"
122
+ },
123
+ "finished_at": {
124
+ "name": "finished_at",
125
+ "type": "timestamptz",
126
+ "unsigned": false,
127
+ "autoincrement": false,
128
+ "primary": false,
129
+ "nullable": true,
130
+ "length": 6,
131
+ "mappedType": "datetime"
132
+ },
133
+ "result_summary": {
134
+ "name": "result_summary",
135
+ "type": "jsonb",
136
+ "unsigned": false,
137
+ "autoincrement": false,
138
+ "primary": false,
139
+ "nullable": true,
140
+ "mappedType": "json"
141
+ },
142
+ "error_message": {
143
+ "name": "error_message",
144
+ "type": "text",
145
+ "unsigned": false,
146
+ "autoincrement": false,
147
+ "primary": false,
148
+ "nullable": true,
149
+ "mappedType": "text"
150
+ },
151
+ "error_stack": {
152
+ "name": "error_stack",
153
+ "type": "text",
154
+ "unsigned": false,
155
+ "autoincrement": false,
156
+ "primary": false,
157
+ "nullable": true,
158
+ "mappedType": "text"
159
+ },
160
+ "meta": {
161
+ "name": "meta",
162
+ "type": "jsonb",
163
+ "unsigned": false,
164
+ "autoincrement": false,
165
+ "primary": false,
166
+ "nullable": true,
167
+ "mappedType": "json"
168
+ },
169
+ "cancellable": {
170
+ "name": "cancellable",
171
+ "type": "boolean",
172
+ "unsigned": false,
173
+ "autoincrement": false,
174
+ "primary": false,
175
+ "nullable": false,
176
+ "default": "false",
177
+ "mappedType": "boolean"
178
+ },
179
+ "cancelled_by_user_id": {
180
+ "name": "cancelled_by_user_id",
181
+ "type": "uuid",
182
+ "unsigned": false,
183
+ "autoincrement": false,
184
+ "primary": false,
185
+ "nullable": true,
186
+ "mappedType": "uuid"
187
+ },
188
+ "cancel_requested_at": {
189
+ "name": "cancel_requested_at",
190
+ "type": "timestamptz",
191
+ "unsigned": false,
192
+ "autoincrement": false,
193
+ "primary": false,
194
+ "nullable": true,
195
+ "length": 6,
196
+ "mappedType": "datetime"
197
+ },
198
+ "parent_job_id": {
199
+ "name": "parent_job_id",
200
+ "type": "uuid",
201
+ "unsigned": false,
202
+ "autoincrement": false,
203
+ "primary": false,
204
+ "nullable": true,
205
+ "mappedType": "uuid"
206
+ },
207
+ "partition_index": {
208
+ "name": "partition_index",
209
+ "type": "int",
210
+ "unsigned": false,
211
+ "autoincrement": false,
212
+ "primary": false,
213
+ "nullable": true,
214
+ "mappedType": "integer"
215
+ },
216
+ "partition_count": {
217
+ "name": "partition_count",
218
+ "type": "int",
219
+ "unsigned": false,
220
+ "autoincrement": false,
221
+ "primary": false,
222
+ "nullable": true,
223
+ "mappedType": "integer"
224
+ },
225
+ "tenant_id": {
226
+ "name": "tenant_id",
227
+ "type": "uuid",
228
+ "unsigned": false,
229
+ "autoincrement": false,
230
+ "primary": false,
231
+ "nullable": false,
232
+ "mappedType": "uuid"
233
+ },
234
+ "organization_id": {
235
+ "name": "organization_id",
236
+ "type": "uuid",
237
+ "unsigned": false,
238
+ "autoincrement": false,
239
+ "primary": false,
240
+ "nullable": true,
241
+ "mappedType": "uuid"
242
+ },
243
+ "created_at": {
244
+ "name": "created_at",
245
+ "type": "timestamptz",
246
+ "unsigned": false,
247
+ "autoincrement": false,
248
+ "primary": false,
249
+ "nullable": false,
250
+ "length": 6,
251
+ "mappedType": "datetime"
252
+ },
253
+ "updated_at": {
254
+ "name": "updated_at",
255
+ "type": "timestamptz",
256
+ "unsigned": false,
257
+ "autoincrement": false,
258
+ "primary": false,
259
+ "nullable": false,
260
+ "length": 6,
261
+ "mappedType": "datetime"
262
+ }
263
+ },
264
+ "name": "progress_jobs",
265
+ "schema": "public",
266
+ "indexes": [
267
+ {
268
+ "keyName": "progress_jobs_parent_idx",
269
+ "columnNames": [
270
+ "parent_job_id"
271
+ ],
272
+ "composite": false,
273
+ "constraint": false,
274
+ "primary": false,
275
+ "unique": false
276
+ },
277
+ {
278
+ "keyName": "progress_jobs_type_tenant_idx",
279
+ "columnNames": [
280
+ "job_type",
281
+ "tenant_id"
282
+ ],
283
+ "composite": true,
284
+ "constraint": false,
285
+ "primary": false,
286
+ "unique": false
287
+ },
288
+ {
289
+ "keyName": "progress_jobs_status_tenant_idx",
290
+ "columnNames": [
291
+ "status",
292
+ "tenant_id"
293
+ ],
294
+ "composite": true,
295
+ "constraint": false,
296
+ "primary": false,
297
+ "unique": false
298
+ },
299
+ {
300
+ "keyName": "progress_jobs_pkey",
301
+ "columnNames": [
302
+ "id"
303
+ ],
304
+ "composite": false,
305
+ "constraint": true,
306
+ "primary": true,
307
+ "unique": true
308
+ }
309
+ ],
310
+ "checks": [],
311
+ "foreignKeys": {},
312
+ "nativeEnums": {}
313
+ }
314
+ ],
315
+ "nativeEnums": {}
316
+ }
@@ -0,0 +1,15 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260220214819 extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ 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"));`);
7
+ this.addSql(`create index "progress_jobs_parent_idx" on "progress_jobs" ("parent_job_id");`);
8
+ this.addSql(`create index "progress_jobs_type_tenant_idx" on "progress_jobs" ("job_type", "tenant_id");`);
9
+ this.addSql(`create index "progress_jobs_status_tenant_idx" on "progress_jobs" ("status", "tenant_id");`);
10
+ }
11
+
12
+ override async down(): Promise<void> {
13
+ this.addSql(`drop table if exists "progress_jobs";`);
14
+ }
15
+ }
@@ -0,0 +1,10 @@
1
+ import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
2
+
3
+ export const setup: ModuleSetupConfig = {
4
+ defaultRoleFeatures: {
5
+ admin: ['progress.*'],
6
+ employee: ['progress.view'],
7
+ },
8
+ }
9
+
10
+ export default setup