@things-factory/worklist 10.1.36 → 10.1.38
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-client/components/activity-instance-preview.js +2 -13
- package/dist-client/components/activity-instance-preview.js.map +1 -1
- package/dist-client/components/activity-thread-timeline.d.ts +2 -2
- package/dist-client/components/can-pick.d.ts +15 -0
- package/dist-client/components/can-pick.js +16 -0
- package/dist-client/components/can-pick.js.map +1 -0
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +1 -0
- package/dist-client/index.js.map +1 -1
- package/dist-client/pages/activity-catalog/activity-catalog-page.js +50 -40
- package/dist-client/pages/activity-catalog/activity-catalog-page.js.map +1 -1
- package/dist-client/pages/activity-catalog/activity-detail-actions.d.ts +34 -0
- package/dist-client/pages/activity-catalog/activity-detail-actions.js +18 -0
- package/dist-client/pages/activity-catalog/activity-detail-actions.js.map +1 -0
- package/dist-client/pages/activity-catalog/activity-detail-page.d.ts +68 -0
- package/dist-client/pages/activity-catalog/activity-detail-page.js +489 -0
- package/dist-client/pages/activity-catalog/activity-detail-page.js.map +1 -0
- package/dist-client/pages/activity-catalog/activity-guide.d.ts +16 -0
- package/dist-client/pages/activity-catalog/activity-guide.js +5 -0
- package/dist-client/pages/activity-catalog/activity-guide.js.map +1 -0
- package/dist-client/pages/activity-catalog/activity-panels.d.ts +80 -0
- package/dist-client/pages/activity-catalog/activity-panels.js +53 -0
- package/dist-client/pages/activity-catalog/activity-panels.js.map +1 -0
- package/dist-client/pages/activity-catalog/activity-stage.d.ts +31 -0
- package/dist-client/pages/activity-catalog/activity-stage.js +44 -0
- package/dist-client/pages/activity-catalog/activity-stage.js.map +1 -0
- package/dist-client/pages/activity-instance/activity-instance-start-page.d.ts +1 -0
- package/dist-client/pages/activity-instance/activity-instance-start-page.js +33 -1
- package/dist-client/pages/activity-instance/activity-instance-start-page.js.map +1 -1
- package/dist-client/pages/activity-instance/discard-offer.d.ts +11 -0
- package/dist-client/pages/activity-instance/discard-offer.js +11 -0
- package/dist-client/pages/activity-instance/discard-offer.js.map +1 -0
- package/dist-client/pages/activity-thread/activity-thread-view.d.ts +1 -1
- package/dist-client/pages/todo/approval-done-list-page.d.ts +5 -1
- package/dist-client/pages/todo/approval-done-list-page.js +8 -1
- package/dist-client/pages/todo/approval-done-list-page.js.map +1 -1
- package/dist-client/pages/todo/approval-pending-list-page.d.ts +5 -1
- package/dist-client/pages/todo/approval-pending-list-page.js +8 -1
- package/dist-client/pages/todo/approval-pending-list-page.js.map +1 -1
- package/dist-client/pages/todo/done-list-page.d.ts +5 -1
- package/dist-client/pages/todo/done-list-page.js +8 -1
- package/dist-client/pages/todo/done-list-page.js.map +1 -1
- package/dist-client/pages/todo/draft-list-page.d.ts +5 -1
- package/dist-client/pages/todo/draft-list-page.js +18 -12
- package/dist-client/pages/todo/draft-list-page.js.map +1 -1
- package/dist-client/pages/todo/list-thumbnail.d.ts +25 -0
- package/dist-client/pages/todo/list-thumbnail.js +25 -0
- package/dist-client/pages/todo/list-thumbnail.js.map +1 -0
- package/dist-client/pages/todo/todo-list-page.d.ts +5 -1
- package/dist-client/pages/todo/todo-list-page.js +8 -1
- package/dist-client/pages/todo/todo-list-page.js.map +1 -1
- package/dist-client/pages/work-door/door-links.d.ts +30 -0
- package/dist-client/pages/work-door/door-links.js +30 -0
- package/dist-client/pages/work-door/door-links.js.map +1 -0
- package/dist-client/pages/work-door/work-doors-page.js +15 -5
- package/dist-client/pages/work-door/work-doors-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +3 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/controllers/activity-approval/delegate.js +27 -2
- package/dist-server/controllers/activity-approval/delegate.js.map +1 -1
- package/dist-server/controllers/activity-approval/delegation.d.ts +31 -0
- package/dist-server/controllers/activity-approval/delegation.js +31 -0
- package/dist-server/controllers/activity-approval/delegation.js.map +1 -0
- package/dist-server/controllers/activity-instance/discard-draft.d.ts +22 -0
- package/dist-server/controllers/activity-instance/discard-draft.js +54 -0
- package/dist-server/controllers/activity-instance/discard-draft.js.map +1 -0
- package/dist-server/controllers/activity-read/assert-may-read.d.ts +15 -0
- package/dist-server/controllers/activity-read/assert-may-read.js +99 -0
- package/dist-server/controllers/activity-read/assert-may-read.js.map +1 -0
- package/dist-server/controllers/activity-read/participates.d.ts +41 -0
- package/dist-server/controllers/activity-read/participates.js +42 -0
- package/dist-server/controllers/activity-read/participates.js.map +1 -0
- package/dist-server/controllers/activity-thread/delegate.js +23 -1
- package/dist-server/controllers/activity-thread/delegate.js.map +1 -1
- package/dist-server/service/activity/activity-mutation.js +1 -0
- package/dist-server/service/activity/activity-mutation.js.map +1 -1
- package/dist-server/service/activity-approval/activity-approval-query.js +7 -1
- package/dist-server/service/activity-approval/activity-approval-query.js.map +1 -1
- package/dist-server/service/activity-approval/activity-approval-subscription.js +2 -11
- package/dist-server/service/activity-approval/activity-approval-subscription.js.map +1 -1
- package/dist-server/service/activity-instance/activity-instance-mutation.js +12 -20
- package/dist-server/service/activity-instance/activity-instance-mutation.js.map +1 -1
- package/dist-server/service/activity-instance/activity-instance-query.js +30 -9
- package/dist-server/service/activity-instance/activity-instance-query.js.map +1 -1
- package/dist-server/service/activity-instance/activity-instance-subscription.js +4 -12
- package/dist-server/service/activity-instance/activity-instance-subscription.js.map +1 -1
- package/dist-server/service/activity-instance/draft-list-states.d.ts +39 -0
- package/dist-server/service/activity-instance/draft-list-states.js +51 -0
- package/dist-server/service/activity-instance/draft-list-states.js.map +1 -0
- package/dist-server/service/activity-instance/index.d.ts +1 -2
- package/dist-server/service/activity-instance/index.js +1 -2
- package/dist-server/service/activity-instance/index.js.map +1 -1
- package/dist-server/service/activity-template/activity-template-mutation.js +6 -0
- package/dist-server/service/activity-template/activity-template-mutation.js.map +1 -1
- package/dist-server/service/activity-thread/activity-thread-mutation.d.ts +0 -3
- package/dist-server/service/activity-thread/activity-thread-mutation.js +0 -19
- package/dist-server/service/activity-thread/activity-thread-mutation.js.map +1 -1
- package/dist-server/service/activity-thread/activity-thread-query.js +7 -1
- package/dist-server/service/activity-thread/activity-thread-query.js.map +1 -1
- package/dist-server/service/activity-thread/activity-thread-subscription.js +2 -8
- package/dist-server/service/activity-thread/activity-thread-subscription.js.map +1 -1
- package/dist-server/service/index.d.ts +2 -2
- package/dist-server/service/subscription/subscription-filters.d.ts +36 -0
- package/dist-server/service/subscription/subscription-filters.js +41 -0
- package/dist-server/service/subscription/subscription-filters.js.map +1 -0
- package/dist-server/service/work-door/work-door-declaration.d.ts +15 -0
- package/dist-server/service/work-door/work-door-declaration.js +18 -0
- package/dist-server/service/work-door/work-door-declaration.js.map +1 -1
- package/dist-server/service/work-door/work-door-resolver.d.ts +3 -0
- package/dist-server/service/work-door/work-door-resolver.js +21 -1
- package/dist-server/service/work-door/work-door-resolver.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/tests/delegate-approval.test.ts +112 -0
- package/tests/discard-draft.test.ts +145 -0
- package/tests/may-read-case.test.ts +166 -0
- package/tests/open-doors.test.ts +274 -0
- package/tests/published-shape.test.ts +85 -0
- package/tests/work-door-not-used-refuse.test.ts +110 -0
- package/things-factory.config.js +1 -0
- package/translations/en.json +32 -3
- package/translations/ja.json +33 -4
- package/translations/ko.json +33 -4
- package/translations/ms.json +32 -3
- package/translations/zh.json +33 -4
- package/dist-server/service/activity-instance/activity-instance-history-query.d.ts +0 -12
- package/dist-server/service/activity-instance/activity-instance-history-query.js +0 -79
- package/dist-server/service/activity-instance/activity-instance-history-query.js.map +0 -1
- package/dist-server/service/activity-instance/activity-instance-history-type.d.ts +0 -5
- package/dist-server/service/activity-instance/activity-instance-history-type.js +0 -21
- package/dist-server/service/activity-instance/activity-instance-history-type.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/worklist",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.38",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"browser": "dist-client/index.js",
|
|
6
6
|
"things-factory": true,
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"@operato/moment-timezone-es": "^10.0.0",
|
|
38
38
|
"@operato/p13n": "^10.0.0",
|
|
39
39
|
"@operato/popup": "^10.15.0",
|
|
40
|
-
"@things-factory/attachment-base": "^10.1.
|
|
41
|
-
"@things-factory/auth-base": "^10.1.
|
|
42
|
-
"@things-factory/board-service": "^10.1.
|
|
43
|
-
"@things-factory/context-ui": "^10.1.
|
|
44
|
-
"@things-factory/organization": "^10.1.
|
|
45
|
-
"@things-factory/personalization": "^10.1.
|
|
46
|
-
"@things-factory/scheduler-client": "^10.1.
|
|
47
|
-
"@things-factory/shell": "^10.1.
|
|
40
|
+
"@things-factory/attachment-base": "^10.1.38",
|
|
41
|
+
"@things-factory/auth-base": "^10.1.38",
|
|
42
|
+
"@things-factory/board-service": "^10.1.38",
|
|
43
|
+
"@things-factory/context-ui": "^10.1.38",
|
|
44
|
+
"@things-factory/organization": "^10.1.38",
|
|
45
|
+
"@things-factory/personalization": "^10.1.38",
|
|
46
|
+
"@things-factory/scheduler-client": "^10.1.38",
|
|
47
|
+
"@things-factory/shell": "^10.1.38"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "b947349fe1c27d11038b592ceadaba336f14785b"
|
|
50
50
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { join } from 'path'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handing an approval seat on, through the real controller on sqlite.
|
|
5
|
+
*
|
|
6
|
+
* What the controller has to carry that the pure decision (test/delegation.test.ts) cannot: the seat
|
|
7
|
+
* keeps what it is on the new row, and the three refusals are asked of the people actually loaded -
|
|
8
|
+
* the receiver's domains, and the issuer read through the thread.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { createSqliteTestDataSource } = require(join(__dirname, '..', '..', 'ops-master', 'test', 'support', 'database.cjs'))
|
|
12
|
+
const DIST = join(__dirname, '..', 'dist-server')
|
|
13
|
+
const { ActivityInstance } = require(join(DIST, 'service', 'activity-instance', 'activity-instance.js'))
|
|
14
|
+
const { ActivityThread } = require(join(DIST, 'service', 'activity-thread', 'activity-thread.js'))
|
|
15
|
+
const { ActivityApproval } = require(join(DIST, 'service', 'activity-approval', 'activity-approval.js'))
|
|
16
|
+
const { delegate } = require(join(DIST, 'controllers', 'activity-approval', 'delegate.js'))
|
|
17
|
+
const { Domain } = require('@things-factory/shell')
|
|
18
|
+
const { User } = require('@things-factory/auth-base')
|
|
19
|
+
|
|
20
|
+
let db: any
|
|
21
|
+
let here: any
|
|
22
|
+
let elsewhere: any
|
|
23
|
+
let holder: any
|
|
24
|
+
let colleague: any
|
|
25
|
+
let issuer: any
|
|
26
|
+
let stranger: any
|
|
27
|
+
|
|
28
|
+
beforeAll(async () => {
|
|
29
|
+
db = createSqliteTestDataSource([ActivityInstance, ActivityThread, ActivityApproval])
|
|
30
|
+
await db.initialize()
|
|
31
|
+
|
|
32
|
+
here = await db.getRepository(Domain).save({ name: 'here', subdomain: 'here' })
|
|
33
|
+
elsewhere = await db.getRepository(Domain).save({ name: 'elsewhere', subdomain: 'elsewhere' })
|
|
34
|
+
|
|
35
|
+
const person = (name: string, domains: any[]) =>
|
|
36
|
+
db.getRepository(User).save({ name, email: `${name}@example.com`, password: 'x', status: 'activated', domains })
|
|
37
|
+
|
|
38
|
+
holder = await person('holder', [here])
|
|
39
|
+
colleague = await person('colleague', [here])
|
|
40
|
+
issuer = await person('issuer', [here])
|
|
41
|
+
stranger = await person('stranger', [elsewhere])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
afterAll(async () => db?.destroy())
|
|
45
|
+
|
|
46
|
+
async function aSeat(participation?: string) {
|
|
47
|
+
const instance = await db.getRepository(ActivityInstance).save({
|
|
48
|
+
domain: here,
|
|
49
|
+
name: 'i',
|
|
50
|
+
state: 'issued',
|
|
51
|
+
issuer,
|
|
52
|
+
approvalLine: [
|
|
53
|
+
{ type: 'User', value: holder.id, ...(participation ? { participation } : {}) },
|
|
54
|
+
{ type: 'User', value: colleague.id }
|
|
55
|
+
]
|
|
56
|
+
})
|
|
57
|
+
const thread = await db.getRepository(ActivityThread).save({ domain: here, name: 't', round: 1, state: 'submitted', activityInstance: instance })
|
|
58
|
+
|
|
59
|
+
return db.getRepository(ActivityApproval).save({
|
|
60
|
+
domain: here,
|
|
61
|
+
activityThread: thread,
|
|
62
|
+
approver: holder,
|
|
63
|
+
order: 1,
|
|
64
|
+
round: 1,
|
|
65
|
+
/* ActivityApprovalJudgment.Pending is the empty string. */
|
|
66
|
+
judgment: '',
|
|
67
|
+
participation
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const handOn = (seat: any, to: any) =>
|
|
72
|
+
db.transaction((tx: any) =>
|
|
73
|
+
delegate({ id: seat.id, to: { id: to.id }, comment: 'away' }, { state: { domain: here, user: holder, tx }, t: (key: string) => key } as any)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const newRowFor = (seat: any, to: any) =>
|
|
77
|
+
db.getRepository(ActivityApproval).findOne({ where: { originalApproval: { id: seat.id }, approver: { id: to.id } } })
|
|
78
|
+
|
|
79
|
+
it('a judging seat goes to a colleague, and the new row is still a judging seat', async () => {
|
|
80
|
+
const seat = await aSeat('approval')
|
|
81
|
+
|
|
82
|
+
await handOn(seat, colleague)
|
|
83
|
+
|
|
84
|
+
expect(await newRowFor(seat, colleague)).toMatchObject({ judgment: '', participation: 'approval' })
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('a reference seat stays a reference once handed on', async () => {
|
|
88
|
+
const seat = await aSeat('reference')
|
|
89
|
+
|
|
90
|
+
await handOn(seat, colleague)
|
|
91
|
+
|
|
92
|
+
expect(await newRowFor(seat, colleague)).toMatchObject({ judgment: '', participation: 'reference' })
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('someone of another tenant is refused, and the seat is still the holder\'s', async () => {
|
|
96
|
+
const seat = await aSeat('approval')
|
|
97
|
+
|
|
98
|
+
await expect(handOn(seat, stranger)).rejects.toThrow('error.activity-approval delegate outside domain')
|
|
99
|
+
expect(await db.getRepository(ActivityApproval).findOne({ where: { id: seat.id } })).toMatchObject({ judgment: '' })
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('a judging seat to whoever raised the case is refused', async () => {
|
|
103
|
+
const seat = await aSeat('approval')
|
|
104
|
+
|
|
105
|
+
await expect(handOn(seat, issuer)).rejects.toThrow('error.activity-approval delegate to issuer')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('to its own holder is refused', async () => {
|
|
109
|
+
const seat = await aSeat('approval')
|
|
110
|
+
|
|
111
|
+
await expect(handOn(seat, holder)).rejects.toThrow('error.activity-approval delegate to self')
|
|
112
|
+
})
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { join } from 'path'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Throwing a draft away (ruling 2026-09-24): whoever wrote it may, nobody else without the management
|
|
5
|
+
* privilege, never once it has gone out - and nothing is left hanging off it.
|
|
6
|
+
*
|
|
7
|
+
* Real entities on sqlite, each call inside a real transaction, so a refusal also shows the rollback:
|
|
8
|
+
* the rows cleared before the refusal are back afterwards.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let mockManages = false
|
|
12
|
+
|
|
13
|
+
jest.mock('@things-factory/auth-base', () => {
|
|
14
|
+
const actual = jest.requireActual('@things-factory/auth-base')
|
|
15
|
+
|
|
16
|
+
return { ...actual, checkPermission: async () => mockManages }
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const { createSqliteTestDataSource } = require(join(__dirname, '..', '..', 'ops-master', 'test', 'support', 'database.cjs'))
|
|
20
|
+
const DIST = join(__dirname, '..', 'dist-server')
|
|
21
|
+
const { ActivityInstance } = require(join(DIST, 'service', 'activity-instance', 'activity-instance.js'))
|
|
22
|
+
const { ActivityThread } = require(join(DIST, 'service', 'activity-thread', 'activity-thread.js'))
|
|
23
|
+
const { ActivityApproval } = require(join(DIST, 'service', 'activity-approval', 'activity-approval.js'))
|
|
24
|
+
const { discardDraft } = require(join(DIST, 'controllers', 'activity-instance', 'discard-draft.js'))
|
|
25
|
+
const { Attachment } = require('@things-factory/attachment-base')
|
|
26
|
+
const { Domain } = require('@things-factory/shell')
|
|
27
|
+
const { User } = require('@things-factory/auth-base')
|
|
28
|
+
|
|
29
|
+
let db: any
|
|
30
|
+
let domain: any
|
|
31
|
+
let writer: any
|
|
32
|
+
let someoneElse: any
|
|
33
|
+
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
db = createSqliteTestDataSource([ActivityInstance, ActivityThread, ActivityApproval, Attachment])
|
|
36
|
+
await db.initialize()
|
|
37
|
+
|
|
38
|
+
domain = await db.getRepository(Domain).save({ name: 'd', subdomain: 'd' })
|
|
39
|
+
writer = await db.getRepository(User).save({ name: 'w', email: 'w@example.com', password: 'x', domains: [domain] })
|
|
40
|
+
someoneElse = await db.getRepository(User).save({ name: 'o', email: 'o@example.com', password: 'x', domains: [domain] })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
afterAll(async () => db?.destroy())
|
|
44
|
+
|
|
45
|
+
beforeEach(() => (mockManages = false))
|
|
46
|
+
|
|
47
|
+
async function aCase(state: string) {
|
|
48
|
+
const instance = await db.getRepository(ActivityInstance).save({ domain, name: 'i', state, creator: writer })
|
|
49
|
+
/* A draft normally has none of these; they stand here so that "nothing left over" means something. */
|
|
50
|
+
const thread = await db.getRepository(ActivityThread).save({ domain, name: 't', round: 1, state: 'assigned', activityInstance: instance })
|
|
51
|
+
await db.getRepository(ActivityApproval).save({ domain, activityThread: thread, order: 1, round: 1, judgment: 'pending' })
|
|
52
|
+
|
|
53
|
+
return instance
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const discardAs = (user: any, id: string) =>
|
|
57
|
+
db.transaction((tx: any) => discardDraft(id, { state: { domain, user, tx }, t: (key: string) => key } as any))
|
|
58
|
+
|
|
59
|
+
const left = async (instanceId: string) => {
|
|
60
|
+
const threads = await db.getRepository(ActivityThread).find({ where: { activityInstance: { id: instanceId } } })
|
|
61
|
+
const approvals = threads.length
|
|
62
|
+
? await db.getRepository(ActivityApproval).find({ where: { activityThread: { id: threads[0].id } } })
|
|
63
|
+
: []
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
instance: !!(await db.getRepository(ActivityInstance).findOne({ where: { id: instanceId } })),
|
|
67
|
+
threads: threads.length,
|
|
68
|
+
approvals: approvals.length
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
it('whoever wrote the draft throws it away, and nothing is left hanging off it', async () => {
|
|
73
|
+
const draft = await aCase('draft')
|
|
74
|
+
|
|
75
|
+
await expect(discardAs(writer, draft.id)).resolves.toBe(true)
|
|
76
|
+
expect(await left(draft.id)).toEqual({ instance: false, threads: 0, approvals: 0 })
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("someone else's draft is refused, and every row is still there", async () => {
|
|
80
|
+
const draft = await aCase('draft')
|
|
81
|
+
|
|
82
|
+
await expect(discardAs(someoneElse, draft.id)).rejects.toThrow('error.activity-instance belongs to someone else')
|
|
83
|
+
expect(await left(draft.id)).toEqual({ instance: true, threads: 1, approvals: 1 })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('one that has gone out is refused by name, and the rows cleared before the refusal come back', async () => {
|
|
87
|
+
const issued = await aCase('issued')
|
|
88
|
+
|
|
89
|
+
await expect(discardAs(writer, issued.id)).rejects.toThrow('error.activity-instance not a draft any more')
|
|
90
|
+
expect(await left(issued.id)).toEqual({ instance: true, threads: 1, approvals: 1 })
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('someone who manages instances may throw away a draft they did not write', async () => {
|
|
94
|
+
const draft = await aCase('draft')
|
|
95
|
+
mockManages = true
|
|
96
|
+
|
|
97
|
+
await expect(discardAs(someoneElse, draft.id)).resolves.toBe(true)
|
|
98
|
+
expect(await left(draft.id)).toEqual({ instance: false, threads: 0, approvals: 0 })
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('an id that is not here is refused by name', async () => {
|
|
102
|
+
await expect(discardAs(writer, '00000000-0000-0000-0000-000000000000')).rejects.toThrow('error.activity-instance not found')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('attachment files and the commit', () => {
|
|
106
|
+
/*
|
|
107
|
+
* deleteAttachmentsByRef removes the files from storage while the transaction is still open. The
|
|
108
|
+
* @transaction directive commits after the resolver returns, so if the commit then fails the rows
|
|
109
|
+
* come back and the files do not. Pointed out 2026-09-24 (architect); there is no after-commit hook
|
|
110
|
+
* in @transaction and no rows-now-files-later shape in attachment-base, so the fix waits for a ruling.
|
|
111
|
+
*
|
|
112
|
+
* test.failing: this is red in intent - it passes while the hole is open, and turns red the day files
|
|
113
|
+
* wait for the commit, which is the signal to make it a plain test.
|
|
114
|
+
*/
|
|
115
|
+
const attachmentConst = require(join(__dirname, '..', '..', 'attachment-base', 'dist-server', 'attachment-const.js'))
|
|
116
|
+
const deletedFiles: string[] = []
|
|
117
|
+
|
|
118
|
+
beforeAll(() => {
|
|
119
|
+
attachmentConst.STORAGE = { ...(attachmentConst.STORAGE || {}), deleteFile: async (path: string) => deletedFiles.push(path) }
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it.failing('a transaction that rolls back after the discard leaves the file in storage', async () => {
|
|
123
|
+
const draft = await aCase('draft')
|
|
124
|
+
await db.getRepository(Attachment).save({
|
|
125
|
+
domain,
|
|
126
|
+
name: 'a.txt',
|
|
127
|
+
refBy: draft.id,
|
|
128
|
+
refType: 'ActivityInstance',
|
|
129
|
+
path: `files/${draft.id}.txt`,
|
|
130
|
+
mimetype: 'text/plain',
|
|
131
|
+
encoding: '7bit',
|
|
132
|
+
size: '1'
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
await expect(
|
|
136
|
+
db.transaction(async (tx: any) => {
|
|
137
|
+
await discardDraft(draft.id, { state: { domain, user: writer, tx }, t: (key: string) => key } as any)
|
|
138
|
+
throw new Error('the commit fails')
|
|
139
|
+
})
|
|
140
|
+
).rejects.toThrow('the commit fails')
|
|
141
|
+
|
|
142
|
+
expect(await left(draft.id)).toEqual({ instance: true, threads: 1, approvals: 1 })
|
|
143
|
+
expect(deletedFiles).not.toContain(`files/${draft.id}.txt`)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { join } from 'path'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The two reads of one case by id actually ask who is reading.
|
|
5
|
+
*
|
|
6
|
+
* `test/participates.test.ts` holds the rule. This holds the wiring: the compiled resolvers are
|
|
7
|
+
* called with the repositories and the privilege check stubbed, so taking the check out of a
|
|
8
|
+
* resolver lets a stranger read somebody else's approval - and this turns red.
|
|
9
|
+
*
|
|
10
|
+
* Stubbed, not faked away: the rows come from the real resolvers' own queries, answered by entity.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const mockRows = new Map<string, any>()
|
|
14
|
+
let mockGrantsEveryCase = false
|
|
15
|
+
|
|
16
|
+
jest.mock('@things-factory/shell', () => {
|
|
17
|
+
const actual = jest.requireActual('@things-factory/shell')
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
getRepository: (entity: any) => ({
|
|
22
|
+
findOne: async ({ where }: any) => {
|
|
23
|
+
const row = mockRows.get(`${entity.name}:${where?.id}`)
|
|
24
|
+
return row && (!where?.domain || row.domainId === where.domain.id) ? row : null
|
|
25
|
+
},
|
|
26
|
+
find: async () => mockRows.get(`${entity.name}:*`) || [],
|
|
27
|
+
findBy: async () => mockRows.get(`${entity.name}:by`) || [],
|
|
28
|
+
findOneBy: async () => null
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
jest.mock('@things-factory/auth-base', () => {
|
|
34
|
+
const actual = jest.requireActual('@things-factory/auth-base')
|
|
35
|
+
|
|
36
|
+
return { ...actual, checkPermission: async () => mockGrantsEveryCase }
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const DIST = join(__dirname, '..', 'dist-server', 'service')
|
|
40
|
+
const { ActivityApprovalQuery } = require(join(DIST, 'activity-approval', 'activity-approval-query.js'))
|
|
41
|
+
const { ActivityThreadQuery } = require(join(DIST, 'activity-thread', 'activity-thread-query.js'))
|
|
42
|
+
const { ActivityInstanceQuery } = require(join(DIST, 'activity-instance', 'activity-instance-query.js'))
|
|
43
|
+
|
|
44
|
+
function contextFor(userId: string, roleIds: string[] = [], roleDomainId = 'd-here') {
|
|
45
|
+
mockRows.set(`User:${userId}`, { id: userId, roles: roleIds.map(id => ({ id, domainId: roleDomainId })) })
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
state: { domain: { id: 'd-here' }, user: { id: userId } },
|
|
49
|
+
t: (key: string) => key
|
|
50
|
+
} as any
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
mockRows.clear()
|
|
55
|
+
mockGrantsEveryCase = false
|
|
56
|
+
|
|
57
|
+
mockRows.set('ActivityInstance:i-1', { id: 'i-1', domainId: 'd-here', issuerId: 'u-issuer' })
|
|
58
|
+
mockRows.set('ActivityThread:t-1', { id: 't-1', domainId: 'd-here', activityInstanceId: 'i-1', assigneeId: 'u-worker' })
|
|
59
|
+
mockRows.set('ActivityApproval:a-1', { id: 'a-1', domainId: 'd-here', activityThreadId: 't-1', approverId: 'u-approver' })
|
|
60
|
+
mockRows.set('ActivityApproval:*', [{ approverId: 'u-approver' }])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('one approval by id', () => {
|
|
64
|
+
const read = (context: any) => new ActivityApprovalQuery().activityApproval('a-1', context)
|
|
65
|
+
|
|
66
|
+
it('a stranger in the same domain is refused by name, not handed a null', async () => {
|
|
67
|
+
await expect(read(contextFor('u-stranger'))).rejects.toThrow('error.activity-case not a participant')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('its approver reads it', async () => {
|
|
71
|
+
await expect(read(contextFor('u-approver'))).resolves.toMatchObject({ id: 'a-1' })
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('whoever raised the case reads it', async () => {
|
|
75
|
+
await expect(read(contextFor('u-issuer'))).resolves.toMatchObject({ id: 'a-1' })
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('a holder of the activity-instance query privilege reads it', async () => {
|
|
79
|
+
mockGrantsEveryCase = true
|
|
80
|
+
await expect(read(contextFor('u-stranger'))).resolves.toMatchObject({ id: 'a-1' })
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('a case that does not exist is still null - absent is not refused', async () => {
|
|
84
|
+
await expect(new ActivityApprovalQuery().activityApproval('a-none', contextFor('u-stranger'))).resolves.toBeNull()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('one thread by id', () => {
|
|
89
|
+
const read = (context: any) => new ActivityThreadQuery().activityThread('t-1', context)
|
|
90
|
+
|
|
91
|
+
it('a stranger in the same domain is refused by name', async () => {
|
|
92
|
+
await expect(read(contextFor('u-stranger'))).rejects.toThrow('error.activity-case not a participant')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('its assignee and its approver read it', async () => {
|
|
96
|
+
await expect(read(contextFor('u-worker'))).resolves.toMatchObject({ id: 't-1' })
|
|
97
|
+
await expect(read(contextFor('u-approver'))).resolves.toMatchObject({ id: 't-1' })
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('a case read under its parent rides on the parent', () => {
|
|
102
|
+
/*
|
|
103
|
+
* dataset's data-ooc and data-sample screens read activityInstance { activityThreads {
|
|
104
|
+
* activityApprovals } } under their own records. These field resolvers do not ask who is reading -
|
|
105
|
+
* the caller already read the parent. Closing the root lists and gating the by-id reads must leave
|
|
106
|
+
* this path as it was.
|
|
107
|
+
*/
|
|
108
|
+
it('the threads under an instance reach a caller who is not a participant', async () => {
|
|
109
|
+
mockRows.set('ActivityThread:by', [{ id: 't-1' }])
|
|
110
|
+
|
|
111
|
+
const threads = await new ActivityInstanceQuery().activityThreads({ id: 'i-1' }, contextFor('u-stranger'))
|
|
112
|
+
expect(threads.map((one: any) => one.id)).toEqual(['t-1'])
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('the approvals under a thread reach a caller who is not a participant', async () => {
|
|
116
|
+
mockRows.set('ActivityApproval:by', [{ id: 'a-1' }])
|
|
117
|
+
|
|
118
|
+
const approvals = await new ActivityThreadQuery().activityApprovals({ id: 't-1' }, contextFor('u-stranger'))
|
|
119
|
+
expect(approvals.map((one: any) => one.id)).toEqual(['a-1'])
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('one instance by id', () => {
|
|
124
|
+
const read = (context: any) => new ActivityInstanceQuery().activityInstance('i-1', context)
|
|
125
|
+
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
mockRows.set('ActivityInstance:i-1', {
|
|
128
|
+
id: 'i-1',
|
|
129
|
+
domainId: 'd-here',
|
|
130
|
+
creatorId: 'u-operator',
|
|
131
|
+
assigneeRoleId: 'r-waiting'
|
|
132
|
+
})
|
|
133
|
+
mockRows.set('ActivityThread:*', [{ id: 't-1', assigneeId: 'u-worker' }])
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('whoever wrote the draft reopens it without the management privilege', async () => {
|
|
137
|
+
await expect(read(contextFor('u-operator'))).resolves.toMatchObject({ id: 'i-1' })
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('a holder of the role it waits for reads it, to pick it up', async () => {
|
|
141
|
+
await expect(read(contextFor('u-picker', ['r-waiting']))).resolves.toMatchObject({ id: 'i-1' })
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it("the assignee of one of its threads and that thread's approver read it", async () => {
|
|
145
|
+
await expect(read(contextFor('u-worker'))).resolves.toMatchObject({ id: 'i-1' })
|
|
146
|
+
await expect(read(contextFor('u-approver'))).resolves.toMatchObject({ id: 'i-1' })
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('a stranger in the same domain is refused by name', async () => {
|
|
150
|
+
await expect(read(contextFor('u-stranger'))).rejects.toThrow('error.activity-case not a participant')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('the waiting or supervisory role counts only when held in this domain', async () => {
|
|
154
|
+
mockRows.set('ActivityInstance:i-1', { id: 'i-1', domainId: 'd-here', assigneeRoleId: 'r-waiting', supervisoryRoleId: 'r-super' })
|
|
155
|
+
|
|
156
|
+
await expect(read(contextFor('u-elsewhere', ['r-waiting', 'r-super'], 'd-other'))).rejects.toThrow(
|
|
157
|
+
'error.activity-case not a participant'
|
|
158
|
+
)
|
|
159
|
+
await expect(read(contextFor('u-here', ['r-super'], 'd-here'))).resolves.toMatchObject({ id: 'i-1' })
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('a holder of the activity-instance query privilege reads it', async () => {
|
|
163
|
+
mockGrantsEveryCase = true
|
|
164
|
+
await expect(read(contextFor('u-stranger'))).resolves.toMatchObject({ id: 'i-1' })
|
|
165
|
+
})
|
|
166
|
+
})
|