@things-factory/organization 10.1.104 → 10.1.105
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-server/service/department/department-mutation.js +5 -12
- package/dist-server/service/department/department-mutation.js.map +1 -1
- package/dist-server/service/department/index.d.ts +1 -1
- package/dist-server/service/domain-reference.d.ts +9 -0
- package/dist-server/service/domain-reference.js +48 -0
- package/dist-server/service/domain-reference.js.map +1 -0
- package/dist-server/service/employee/employee-mutation.js +13 -37
- package/dist-server/service/employee/employee-mutation.js.map +1 -1
- package/dist-server/service/employee/index.d.ts +1 -1
- package/dist-server/service/index.d.ts +2 -2
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/tests/domain-reference.test.ts +184 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/organization",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.105",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"browser": "dist-client/index.js",
|
|
6
6
|
"things-factory": true,
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"@operato/popup": "^10.15.0",
|
|
38
38
|
"@operato/shell": "^10.0.0",
|
|
39
39
|
"@things-factory/auth-base": "^10.1.79",
|
|
40
|
-
"@things-factory/code-ui": "^10.1.
|
|
41
|
-
"@things-factory/contact": "^10.1.
|
|
40
|
+
"@things-factory/code-ui": "^10.1.105",
|
|
41
|
+
"@things-factory/contact": "^10.1.105",
|
|
42
42
|
"@things-factory/shell": "^10.1.79"
|
|
43
43
|
},
|
|
44
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "c8ad9b40d57709beefd5e3d6167fd9cab6efea3a"
|
|
45
45
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A department's manager and an employee's supervisor, department and user are looked up inside the caller's domain.
|
|
3
|
+
*
|
|
4
|
+
* The lookups asked by id alone, so a mutation run in one domain could link a row of another: an employee of
|
|
5
|
+
* domain B as the manager of a department of domain A, a user who is not a member of A as an employee of A.
|
|
6
|
+
* This runs the real mutations on sqlite and reads the links back from the table.
|
|
7
|
+
*/
|
|
8
|
+
import 'reflect-metadata'
|
|
9
|
+
|
|
10
|
+
const { existsSync } = require('fs')
|
|
11
|
+
const { join } = require('path')
|
|
12
|
+
const { DataSource } = require('typeorm')
|
|
13
|
+
|
|
14
|
+
const shell = require('@things-factory/shell')
|
|
15
|
+
const authBase = require('@things-factory/auth-base')
|
|
16
|
+
const contact = require('@things-factory/contact')
|
|
17
|
+
|
|
18
|
+
function loadCompiled(modulePath: string): any {
|
|
19
|
+
const resolved = join(__dirname, '..', 'dist-server', `${modulePath}.js`)
|
|
20
|
+
if (!existsSync(resolved)) {
|
|
21
|
+
throw new Error(`organization build output is missing: ${resolved}\nBuild it first: npm run build:server`)
|
|
22
|
+
}
|
|
23
|
+
return require(resolved)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let dataSource: any
|
|
27
|
+
let Department: any
|
|
28
|
+
let Employee: any
|
|
29
|
+
let DepartmentMutation: any
|
|
30
|
+
let EmployeeMutation: any
|
|
31
|
+
|
|
32
|
+
let domain: any
|
|
33
|
+
let other: any
|
|
34
|
+
let user: any
|
|
35
|
+
let member: any
|
|
36
|
+
let outsider: any
|
|
37
|
+
let ours: any
|
|
38
|
+
let theirs: any
|
|
39
|
+
let ourTeam: any
|
|
40
|
+
let theirTeam: any
|
|
41
|
+
|
|
42
|
+
beforeAll(async () => {
|
|
43
|
+
const organization = loadCompiled('service/index')
|
|
44
|
+
Department = loadCompiled('service/department/department').Department
|
|
45
|
+
Employee = loadCompiled('service/employee/employee').Employee
|
|
46
|
+
DepartmentMutation = loadCompiled('service/department/department-mutation').DepartmentMutation
|
|
47
|
+
EmployeeMutation = loadCompiled('service/employee/employee-mutation').EmployeeMutation
|
|
48
|
+
|
|
49
|
+
dataSource = new DataSource({
|
|
50
|
+
type: 'sqlite',
|
|
51
|
+
database: ':memory:',
|
|
52
|
+
synchronize: true,
|
|
53
|
+
dropSchema: true,
|
|
54
|
+
namingStrategy: new shell.NamingStrategy(),
|
|
55
|
+
entities: [...shell.entities, ...authBase.entities, ...contact.entities, ...organization.entities],
|
|
56
|
+
logging: false
|
|
57
|
+
})
|
|
58
|
+
await dataSource.initialize()
|
|
59
|
+
shell.addDataSource('default', dataSource)
|
|
60
|
+
shell.addDataSource('tx', dataSource)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
beforeEach(async () => {
|
|
64
|
+
await dataSource.synchronize(true)
|
|
65
|
+
const domains = dataSource.getRepository(shell.Domain)
|
|
66
|
+
domain = await domains.save({ name: 'ours', subdomain: 'ours' })
|
|
67
|
+
other = await domains.save({ name: 'theirs', subdomain: 'theirs' })
|
|
68
|
+
|
|
69
|
+
const users = dataSource.getRepository(authBase.User)
|
|
70
|
+
user = await users.save({ name: 'tester', email: 'tester@domain-reference.test', domains: [domain] })
|
|
71
|
+
member = await users.save({ name: 'member', email: 'member@domain-reference.test', domains: [domain] })
|
|
72
|
+
outsider = await users.save({ name: 'outsider', email: 'outsider@domain-reference.test', domains: [other] })
|
|
73
|
+
|
|
74
|
+
const employees = dataSource.getRepository(Employee)
|
|
75
|
+
ours = await employees.save({ domain, controlNo: 'E1', name: 'ours' })
|
|
76
|
+
theirs = await employees.save({ domain: other, controlNo: 'E1', name: 'theirs' })
|
|
77
|
+
|
|
78
|
+
const departments = dataSource.getRepository(Department)
|
|
79
|
+
ourTeam = await departments.save({ domain, controlNo: 'T1', name: 'our team' })
|
|
80
|
+
theirTeam = await departments.save({ domain: other, controlNo: 'T1', name: 'their team' })
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
afterAll(async () => {
|
|
84
|
+
if (!dataSource) return
|
|
85
|
+
shell.removeDataSource('default')
|
|
86
|
+
shell.removeDataSource('tx')
|
|
87
|
+
await dataSource.destroy()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const context = () => ({ state: { domain, user, tx: dataSource.manager } })
|
|
91
|
+
|
|
92
|
+
async function managerOf(id: string) {
|
|
93
|
+
return (await dataSource.getRepository(Department).findOne({ where: { id }, relations: ['manager'] }))?.manager?.id ?? null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function linksOf(id: string) {
|
|
97
|
+
const row = await dataSource.getRepository(Employee).findOne({ where: { id }, relations: ['supervisor', 'department', 'user'] })
|
|
98
|
+
return { supervisor: row?.supervisor?.id ?? null, department: row?.department?.id ?? null, user: row?.user?.id ?? null }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe("a department's manager comes from the caller's domain", () => {
|
|
102
|
+
it('an employee of this domain is accepted', async () => {
|
|
103
|
+
const created = await new DepartmentMutation().createDepartment({ controlNo: 'T2', name: 'new', manager: { id: ours.id } }, context())
|
|
104
|
+
expect(await managerOf(created.id)).toBe(ours.id)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('createDepartment refuses an employee of another domain', async () => {
|
|
108
|
+
await expect(
|
|
109
|
+
new DepartmentMutation().createDepartment({ controlNo: 'T2', name: 'new', manager: { id: theirs.id } }, context())
|
|
110
|
+
).rejects.toThrow(/manager not found in this domain/)
|
|
111
|
+
expect(await dataSource.getRepository(Department).countBy({ name: 'new' })).toBe(0)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('updateDepartment refuses an employee of another domain', async () => {
|
|
115
|
+
await expect(
|
|
116
|
+
new DepartmentMutation().updateDepartment(ourTeam.id, { manager: { id: theirs.id } }, context())
|
|
117
|
+
).rejects.toThrow(/manager not found in this domain/)
|
|
118
|
+
expect(await managerOf(ourTeam.id)).toBeNull()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('updateMultipleDepartment refuses it on a new row and on an edited one', async () => {
|
|
122
|
+
const mutation = new DepartmentMutation()
|
|
123
|
+
await expect(
|
|
124
|
+
mutation.updateMultipleDepartment([{ cuFlag: '+', controlNo: 'T2', name: 'new', manager: { id: theirs.id } }], context())
|
|
125
|
+
).rejects.toThrow(/manager not found in this domain/)
|
|
126
|
+
await expect(
|
|
127
|
+
mutation.updateMultipleDepartment([{ cuFlag: 'M', id: ourTeam.id, manager: { id: theirs.id } }], context())
|
|
128
|
+
).rejects.toThrow(/manager not found in this domain/)
|
|
129
|
+
expect(await managerOf(ourTeam.id)).toBeNull()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('null still clears the manager', async () => {
|
|
133
|
+
await new DepartmentMutation().updateDepartment(ourTeam.id, { manager: { id: ours.id } }, context())
|
|
134
|
+
await new DepartmentMutation().updateDepartment(ourTeam.id, { manager: null }, context())
|
|
135
|
+
expect(await managerOf(ourTeam.id)).toBeNull()
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe("an employee's supervisor, department and user come from the caller's domain", () => {
|
|
140
|
+
it('rows of this domain, and a member of it, are accepted', async () => {
|
|
141
|
+
const created = await new EmployeeMutation().createEmployee(
|
|
142
|
+
{ controlNo: 'E2', name: 'new', supervisor: { id: ours.id }, department: { id: ourTeam.id }, user: { id: member.id } },
|
|
143
|
+
context()
|
|
144
|
+
)
|
|
145
|
+
expect(await linksOf(created.id)).toEqual({ supervisor: ours.id, department: ourTeam.id, user: member.id })
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it.each([
|
|
149
|
+
['supervisor', () => ({ supervisor: { id: theirs.id } })],
|
|
150
|
+
['department', () => ({ department: { id: theirTeam.id } })],
|
|
151
|
+
['user', () => ({ user: { id: outsider.id } })]
|
|
152
|
+
])('createEmployee refuses a %s of another domain', async (what, patch) => {
|
|
153
|
+
await expect(
|
|
154
|
+
new EmployeeMutation().createEmployee({ controlNo: 'E2', name: 'new', ...patch() }, context())
|
|
155
|
+
).rejects.toThrow(new RegExp(`${what} not found in this domain`))
|
|
156
|
+
expect(await dataSource.getRepository(Employee).countBy({ controlNo: 'E2' })).toBe(0)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it.each([
|
|
160
|
+
['supervisor', () => ({ supervisor: { id: theirs.id } })],
|
|
161
|
+
['department', () => ({ department: { id: theirTeam.id } })],
|
|
162
|
+
['user', () => ({ user: { id: outsider.id } })]
|
|
163
|
+
])('updateEmployee refuses a %s of another domain', async (what, patch) => {
|
|
164
|
+
await expect(new EmployeeMutation().updateEmployee(ours.id, patch(), context())).rejects.toThrow(
|
|
165
|
+
new RegExp(`${what} not found in this domain`)
|
|
166
|
+
)
|
|
167
|
+
expect(await linksOf(ours.id)).toEqual({ supervisor: null, department: null, user: null })
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it.each([
|
|
171
|
+
['supervisor', () => ({ supervisor: { id: theirs.id } })],
|
|
172
|
+
['department', () => ({ department: { id: theirTeam.id } })],
|
|
173
|
+
['user', () => ({ user: { id: outsider.id } })]
|
|
174
|
+
])('updateMultipleEmployee refuses a %s of another domain on a new row and on an edited one', async (what, patch) => {
|
|
175
|
+
const mutation = new EmployeeMutation()
|
|
176
|
+
await expect(
|
|
177
|
+
mutation.updateMultipleEmployee([{ cuFlag: '+', controlNo: 'E2', name: 'new', ...patch() }], context())
|
|
178
|
+
).rejects.toThrow(new RegExp(`${what} not found in this domain`))
|
|
179
|
+
await expect(
|
|
180
|
+
mutation.updateMultipleEmployee([{ cuFlag: 'M', id: ours.id, ...patch() }], context())
|
|
181
|
+
).rejects.toThrow(new RegExp(`${what} not found in this domain`))
|
|
182
|
+
expect(await linksOf(ours.id)).toEqual({ supervisor: null, department: null, user: null })
|
|
183
|
+
})
|
|
184
|
+
})
|