@things-factory/headless-twin 10.1.24 → 10.1.26
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/actuation/actuation-advisor.d.ts +2 -0
- package/dist-server/service/actuation/actuation-advisor.js +1 -1
- package/dist-server/service/actuation/actuation-advisor.js.map +1 -1
- package/dist-server/service/actuation/twin-command-resolver.js +1 -1
- package/dist-server/service/actuation/twin-command-resolver.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +38 -2
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/package.json +8 -8
- package/server/service/actuation/actuation-advisor.ts +8 -1
- package/server/service/actuation/twin-command-resolver.ts +1 -1
- package/server/service/twin-model/twin-model-item-query.ts +37 -2
- package/test/actuation-advisor.test.ts +55 -0
- package/test/twin-model-parent-relation-db.test.ts +125 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A location's parent is another location or an area — the item panel splits them by lookup.
|
|
3
|
+
*
|
|
4
|
+
* Contract 0.9.24 (ops-contract `00a346f`) declares `locations.parentId` as `axis-or-external`: the parent is
|
|
5
|
+
* another location (line · cell · zone) or, when no location has that id, an area of the space. Before this, the
|
|
6
|
+
* declaration said "area" only, so a bin under a zone could not walk to its zone, and a zone could not see the
|
|
7
|
+
* bins under it.
|
|
8
|
+
*
|
|
9
|
+
* What is held here:
|
|
10
|
+
* - a parent that is a location becomes a link to that location, and nothing of it is sent to the area resolver;
|
|
11
|
+
* - a parent that is not a location is resolved as an area — only those ids reach the resolver;
|
|
12
|
+
* - a parent id found nowhere comes back as the resolver answers it (missing), not as an empty relation;
|
|
13
|
+
* - a zone sees the locations under it as incoming, counted from the document (rows carry no parent location).
|
|
14
|
+
*/
|
|
15
|
+
import { test, before, after } from 'node:test'
|
|
16
|
+
import assert from 'node:assert/strict'
|
|
17
|
+
import { createRequire } from 'node:module'
|
|
18
|
+
import type { DataSource } from 'typeorm'
|
|
19
|
+
|
|
20
|
+
const req = createRequire(import.meta.url)
|
|
21
|
+
const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
|
|
22
|
+
const { entities: authEntities } = req('@things-factory/auth-base')
|
|
23
|
+
const { entities: twinEntities } = req('../dist-server/service/index.js')
|
|
24
|
+
const { TwinEngine } = req('../dist-server/engine/index.js')
|
|
25
|
+
const { TwinModelItemQuery } = req('../dist-server/service/twin-model/twin-model-item-query.js')
|
|
26
|
+
const { registerExternalResolver, clearExternalResolvers } = req('../dist-server/service/twin-model/external-resolver.js')
|
|
27
|
+
const { DataSource: TypeOrmDataSource } = req('typeorm')
|
|
28
|
+
|
|
29
|
+
let ds: DataSource
|
|
30
|
+
let domainId: string
|
|
31
|
+
let q: any
|
|
32
|
+
let ctx: any
|
|
33
|
+
const asked: string[][] = []
|
|
34
|
+
|
|
35
|
+
const INSTANCE = 'parent-probe'
|
|
36
|
+
|
|
37
|
+
const master = (): any => ({
|
|
38
|
+
source: INSTANCE,
|
|
39
|
+
system: 'wms',
|
|
40
|
+
siteName: 'parent site',
|
|
41
|
+
space: { width: 80, depth: 40, unit: 'm' },
|
|
42
|
+
locations: [
|
|
43
|
+
{ id: 'hall-a', type: 'storage', role: 'area', name: 'Hall A' },
|
|
44
|
+
{ id: 'zone-1', type: 'storage', role: 'location', name: 'Zone 1', parentId: 'hall-a' },
|
|
45
|
+
{ id: 'bin-1', type: 'storage', role: 'location', name: 'Bin 1', parentId: 'zone-1', capacity: 10 },
|
|
46
|
+
{ id: 'bin-2', type: 'storage', role: 'location', name: 'Bin 2', parentId: 'zone-1', capacity: 10 },
|
|
47
|
+
{ id: 'bin-x', type: 'storage', role: 'location', name: 'Bin X', parentId: 'nowhere', capacity: 10 }
|
|
48
|
+
],
|
|
49
|
+
equipment: [{ id: 'fk-1', kind: 'forklift', homeLocationId: 'zone-1' }]
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
before(async () => {
|
|
53
|
+
ds = new TypeOrmDataSource({
|
|
54
|
+
type: 'sqlite',
|
|
55
|
+
database: ':memory:',
|
|
56
|
+
synchronize: true,
|
|
57
|
+
dropSchema: true,
|
|
58
|
+
namingStrategy: new NamingStrategy(),
|
|
59
|
+
entities: [...shellEntities, ...authEntities, ...twinEntities],
|
|
60
|
+
logging: false
|
|
61
|
+
})
|
|
62
|
+
await ds.initialize()
|
|
63
|
+
addDataSource('default', ds)
|
|
64
|
+
const d = await ds.getRepository(Domain).save({ name: 'parent-test', subdomain: 'parent-test' } as any)
|
|
65
|
+
domainId = d.id
|
|
66
|
+
q = new TwinModelItemQuery()
|
|
67
|
+
ctx = { state: { domain: { id: domainId } } }
|
|
68
|
+
|
|
69
|
+
/* A stand-in for the host's area resolver: it records what it was asked and knows one area. */
|
|
70
|
+
registerExternalResolver({
|
|
71
|
+
entity: 'space.area',
|
|
72
|
+
resolve: async (_domainId: string, ids: string[]) => {
|
|
73
|
+
asked.push([...ids])
|
|
74
|
+
return ids.map(id => (id === 'hall-a' ? { id, name: 'Hall A' } : { id, missing: true }))
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
await TwinEngine.ingestMaster(domainId, master(), 'reset')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
after(async () => {
|
|
82
|
+
clearExternalResolvers()
|
|
83
|
+
await TwinEngine.stop(domainId, INSTANCE).catch(() => {})
|
|
84
|
+
await ds?.destroy()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const parentLinks = (item: any) => item.links.out.filter((l: any) => l.field === 'parentId')
|
|
88
|
+
|
|
89
|
+
test('a parent that is a location is a link to that location — none of it goes to the area resolver', async () => {
|
|
90
|
+
asked.length = 0
|
|
91
|
+
const bin = await q.twinModelItem(INSTANCE, 'locations', 'bin-1', ctx)
|
|
92
|
+
const links = parentLinks(bin)
|
|
93
|
+
assert.equal(links.length, 1, `expected one parent line, got ${JSON.stringify(links)}`)
|
|
94
|
+
assert.deepEqual(links[0].target, { kind: 'axis', axis: 'locations' })
|
|
95
|
+
assert.equal(links[0].declaredTarget.kind, 'axis-or-external')
|
|
96
|
+
assert.deepEqual(links[0].ids, ['zone-1'])
|
|
97
|
+
assert.deepEqual(asked, [], 'a location parent was sent to the area resolver')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('a parent that is not a location is resolved as an area — only that id reaches the resolver', async () => {
|
|
101
|
+
asked.length = 0
|
|
102
|
+
const zone = await q.twinModelItem(INSTANCE, 'locations', 'zone-1', ctx)
|
|
103
|
+
const links = parentLinks(zone)
|
|
104
|
+
assert.equal(links.length, 1)
|
|
105
|
+
assert.deepEqual(links[0].target, { kind: 'external', entity: 'space.area' })
|
|
106
|
+
assert.deepEqual(links[0].refs, [{ id: 'hall-a', name: 'Hall A' }])
|
|
107
|
+
assert.deepEqual(asked, [['hall-a']])
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('a parent found nowhere is said to be missing — not an empty relation', async () => {
|
|
111
|
+
const bin = await q.twinModelItem(INSTANCE, 'locations', 'bin-x', ctx)
|
|
112
|
+
const links = parentLinks(bin)
|
|
113
|
+
assert.equal(links.length, 1)
|
|
114
|
+
assert.equal(links[0].target.kind, 'external')
|
|
115
|
+
assert.equal(links[0].total, 1)
|
|
116
|
+
assert.deepEqual(links[0].refs, [{ id: 'nowhere', missing: true }])
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('a zone sees the locations under it as incoming — counted from the document', async () => {
|
|
120
|
+
const zone = await q.twinModelItem(INSTANCE, 'locations', 'zone-1', ctx)
|
|
121
|
+
const under = zone.links.incoming.find((l: any) => l.from === 'locations' && l.field === 'parentId')
|
|
122
|
+
assert.ok(under, `no incoming parent relation on the zone: ${JSON.stringify(zone.links.incoming.map((l: any) => [l.from, l.field]))}`)
|
|
123
|
+
assert.deepEqual([...under.ids].sort(), ['bin-1', 'bin-2'])
|
|
124
|
+
assert.equal(under.total, 2)
|
|
125
|
+
})
|