@things-factory/kpi 10.1.38 → 10.1.40

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/kpi",
3
- "version": "10.1.38",
3
+ "version": "10.1.40",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@operato/app": "^10.0.0",
32
- "@operato/data-grist": "^10.0.0",
32
+ "@operato/data-grist": "^10.19.0",
33
33
  "@operato/dataset": "^10.0.0",
34
34
  "@operato/ghost-print": "^10.0.0",
35
35
  "@operato/graphql": "^10.0.0",
@@ -41,9 +41,9 @@
41
41
  "@operato/shell": "^10.0.0",
42
42
  "@operato/styles": "^10.0.0",
43
43
  "@operato/utils": "^10.0.0",
44
- "@things-factory/auth-base": "^10.1.38",
45
- "@things-factory/dataset": "^10.1.38",
46
- "@things-factory/shell": "^10.1.38"
44
+ "@things-factory/auth-base": "^10.1.40",
45
+ "@things-factory/dataset": "^10.1.40",
46
+ "@things-factory/shell": "^10.1.40"
47
47
  },
48
- "gitHead": "b947349fe1c27d11038b592ceadaba336f14785b"
48
+ "gitHead": "51c5d804c14ad8fcc6d4707e6ef80c9deede2766"
49
49
  }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * kpi's tables build on sqlite - the development default.
3
+ *
4
+ * KpiMetric.collectType was declared type 'enum', a name only postgres and mysql know. The sqlite driver
5
+ * refused it when the DataSource opened (DataTypeNotSupportedError), so no app with kpi could start on
6
+ * sqlite, and list-search-db could only run on postgres. It is 'simple-enum' now: postgres and mysql turn
7
+ * it into the same native enum as before (an existing postgres table sees no schema change), sqlite a
8
+ * varchar with a CHECK.
9
+ *
10
+ * Needs kpi's dist-server, for the same reason list-search-db does:
11
+ *
12
+ * yarn workspace @things-factory/kpi build:server
13
+ * yarn _test packages/kpi/tests
14
+ */
15
+
16
+ import type { DataSource } from 'typeorm'
17
+ import { existsSync } from 'fs'
18
+ import { join } from 'path'
19
+
20
+ /* Entities pick their column types from ormconfig.type when they load, so the driver is named first. */
21
+ require('@things-factory/env').config.set('ormconfig', { type: 'sqlite' })
22
+
23
+ const { DataSource: TypeOrmDataSource } = require('typeorm')
24
+ const shell = require('@things-factory/shell')
25
+ const authBase = require('@things-factory/auth-base')
26
+ /* The same closed entity graph list-search-db builds: KpiMetric -> DataSet -> Scenario, worklist. */
27
+ const integration = require('@things-factory/integration-base')
28
+ const worklist = require('@things-factory/worklist')
29
+ const dataset = require('@things-factory/dataset')
30
+ const attachment = require('@things-factory/attachment-base')
31
+
32
+ const DIST = join(__dirname, '..', 'dist-server')
33
+ if (!existsSync(join(DIST, 'service', 'index.js'))) {
34
+ throw new Error(`kpi's dist-server is missing - run "yarn workspace @things-factory/kpi build:server" first`)
35
+ }
36
+ const kpi = require(join(DIST, 'service', 'index.js'))
37
+
38
+ let dataSource: DataSource
39
+ let domain: any
40
+
41
+ describe('kpi entities on sqlite', () => {
42
+ beforeAll(async () => {
43
+ dataSource = new TypeOrmDataSource({
44
+ type: 'sqlite',
45
+ database: ':memory:',
46
+ synchronize: true,
47
+ dropSchema: true,
48
+ namingStrategy: new shell.NamingStrategy(),
49
+ entities: [
50
+ ...shell.entities,
51
+ ...authBase.entities,
52
+ ...attachment.entities,
53
+ ...integration.entities,
54
+ ...worklist.entities,
55
+ ...dataset.entities,
56
+ ...kpi.entities
57
+ ],
58
+ logging: false
59
+ })
60
+ await dataSource.initialize()
61
+ domain = await dataSource.getRepository(shell.Domain).save({ name: 'Plant A', subdomain: 'plant-a' })
62
+ })
63
+
64
+ afterAll(async () => {
65
+ if (dataSource?.isInitialized) await dataSource.destroy()
66
+ })
67
+
68
+ it('builds every kpi table', async () => {
69
+ const tables = (await dataSource.query(`SELECT name FROM sqlite_master WHERE type = 'table'`)).map(
70
+ (row: any) => row.name
71
+ )
72
+ for (const entity of kpi.entities) {
73
+ expect(tables).toContain(dataSource.getMetadata(entity).tableName)
74
+ }
75
+ })
76
+
77
+ it('a metric takes AUTO when no collect type is given, and keeps the one it is given', async () => {
78
+ const repo = dataSource.getRepository(kpi.KpiMetric)
79
+ const auto = await repo.save({ domain, name: 'Downtime minutes' })
80
+ const manual = await repo.save({ domain, name: 'Good units', collectType: kpi.KpiMetricCollectType.MANUAL })
81
+ expect((await repo.findOneBy({ id: auto.id })).collectType).toBe('AUTO')
82
+ expect((await repo.findOneBy({ id: manual.id })).collectType).toBe('MANUAL')
83
+ })
84
+
85
+ it('refuses a collect type outside the enum - the column still holds only its values', async () => {
86
+ const table = dataSource.getMetadata(kpi.KpiMetric).tableName
87
+ await dataSource.getRepository(kpi.KpiMetric).save({ domain, name: 'Scrap count' })
88
+ await expect(
89
+ dataSource.query(`UPDATE "${table}" SET "collect_type" = 'SOMETHING_ELSE'`)
90
+ ).rejects.toThrow(/CHECK constraint failed/)
91
+ })
92
+ })
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The two value lists answer the title bar search - against a real database.
3
+ *
4
+ * The screens send the term itself (`search-mode="term"`, ADR-0060 decision 10) and the door expands
5
+ * it over its own searchables. A pure test cannot see whether that list is one the entity can answer:
6
+ * a name the entity does not have throws, and a uuid column is an error on postgres (LOWER(uuid) has
7
+ * no function). kpiMetricValues had both - metricId and a group the entity never had - so the search
8
+ * would have answered with an error instead of rows.
9
+ *
10
+ * Postgres only, with TEST_POSTGRES_URL set - a throwaway container is enough, synchronize builds the
11
+ * tables:
12
+ *
13
+ * docker run -d --rm --name kpi-pg -e POSTGRES_PASSWORD=x -p 55433:5432 postgres:14-alpine
14
+ * TEST_POSTGRES_URL=postgres://postgres:x@localhost:55433/postgres yarn _test packages/kpi/tests
15
+ *
16
+ * Not sqlite: sqlite lowers a uuid without complaint, so it cannot show the defect. (It builds these
17
+ * tables since KpiMetric.collectType became 'simple-enum' - entities-sqlite.test.ts keeps that so.)
18
+ * Without the variable the suite is skipped, and says so.
19
+ */
20
+
21
+ import type { DataSource } from 'typeorm'
22
+ import { existsSync } from 'fs'
23
+ import { join } from 'path'
24
+
25
+ const POSTGRES = process.env.TEST_POSTGRES_URL
26
+
27
+ /* Entities pick their column types from ormconfig.type when they load (attachment's contents is blob
28
+ or bytea), so the driver is named before any of them is required. */
29
+ require('@things-factory/env').config.set('ormconfig', { type: 'postgres' })
30
+
31
+ const { DataSource: TypeOrmDataSource } = require('typeorm')
32
+ const shell = require('@things-factory/shell')
33
+ const authBase = require('@things-factory/auth-base')
34
+ /* KpiMetric points at dataset's DataSet, which points at integration's Scenario - the entity graph
35
+ has to close before the tables can be built (dataset's out-of-control reviews reach worklist). */
36
+ const integration = require('@things-factory/integration-base')
37
+ const worklist = require('@things-factory/worklist')
38
+ const dataset = require('@things-factory/dataset')
39
+ const attachment = require('@things-factory/attachment-base')
40
+
41
+ const DIST = join(__dirname, '..', 'dist-server')
42
+ if (!existsSync(join(DIST, 'service', 'index.js'))) {
43
+ throw new Error(`kpi's dist-server is missing - run "yarn workspace @things-factory/kpi build:server" first`)
44
+ }
45
+ /* The entities use type-graphql decorators that need tsc's metadata, which babel cannot give -
46
+ the same reason auth-base's DB tests load the compiled output (auth-base/tests/compiled.ts). */
47
+ const kpi = require(join(DIST, 'service', 'index.js'))
48
+ const { KpiValueQuery } = require(join(DIST, 'service', 'kpi-value', 'kpi-value-query.js'))
49
+ const { KpiMetricValueQuery } = require(join(DIST, 'service', 'kpi-metric-value', 'kpi-metric-value-query.js'))
50
+
51
+ let dataSource: DataSource
52
+ let domain: any
53
+
54
+ const onPostgres = POSTGRES ? describe : describe.skip
55
+
56
+ const context = () => ({ state: { domain } }) as any
57
+ const repo = (entity: any) => dataSource.getRepository(entity)
58
+
59
+ onPostgres('the value lists answer the title bar search (postgres, TEST_POSTGRES_URL)', () => {
60
+ beforeAll(async () => {
61
+ dataSource = new TypeOrmDataSource({
62
+ type: 'postgres',
63
+ url: POSTGRES,
64
+ synchronize: true,
65
+ dropSchema: true,
66
+ namingStrategy: new shell.NamingStrategy(),
67
+ entities: [
68
+ ...shell.entities,
69
+ ...authBase.entities,
70
+ ...attachment.entities,
71
+ ...integration.entities,
72
+ ...worklist.entities,
73
+ ...dataset.entities,
74
+ ...kpi.entities
75
+ ],
76
+ logging: false
77
+ })
78
+ await dataSource.initialize()
79
+ shell.addDataSource('default', dataSource)
80
+ })
81
+
82
+ afterAll(async () => {
83
+ if (dataSource?.isInitialized) await dataSource.destroy()
84
+ })
85
+
86
+ beforeEach(async () => {
87
+ await dataSource.synchronize(true)
88
+ domain = await repo(shell.Domain).save({ name: 'Plant A', subdomain: 'plant-a' })
89
+ })
90
+
91
+ describe('kpiValues', () => {
92
+ async function seed() {
93
+ const scrap = await repo(kpi.Kpi).save({ domain, name: 'Scrap rate' })
94
+ const oee = await repo(kpi.Kpi).save({ domain, name: 'OEE' })
95
+ await repo(kpi.KpiValue).save([
96
+ { domain, kpi: scrap, version: 1, valueDate: '2026-09-01', value: 1.2, group: 'line-1' },
97
+ { domain, kpi: oee, version: 1, valueDate: '2026-09-02', value: 81, group: 'line-2' }
98
+ ])
99
+ }
100
+
101
+ it('finds a value by the name of its KPI', async () => {
102
+ await seed()
103
+ const { items, total } = await new KpiValueQuery().kpiValues({ search: 'scrap' }, context())
104
+ expect(total).toBe(1)
105
+ expect(items[0].valueDate).toBe('2026-09-01')
106
+ })
107
+
108
+ it('finds a value by its group and by its date', async () => {
109
+ await seed()
110
+ expect((await new KpiValueQuery().kpiValues({ search: 'line-2' }, context())).total).toBe(1)
111
+ expect((await new KpiValueQuery().kpiValues({ search: '09-01' }, context())).total).toBe(1)
112
+ })
113
+
114
+ it('answers the whole list without a term', async () => {
115
+ await seed()
116
+ expect((await new KpiValueQuery().kpiValues({}, context())).total).toBe(2)
117
+ })
118
+ })
119
+
120
+ describe('kpiMetricValues', () => {
121
+ async function seed() {
122
+ const downtime = await repo(kpi.KpiMetric).save({ domain, name: 'Downtime minutes' })
123
+ const output = await repo(kpi.KpiMetric).save({ domain, name: 'Good units' })
124
+ await repo(kpi.KpiMetricValue).save([
125
+ { domain, metric: downtime, valueDate: '2026-09-01', value: 42 },
126
+ { domain, metric: output, valueDate: '2026-09-02', value: 900 }
127
+ ])
128
+ }
129
+
130
+ it('finds a value by the name of its metric - an answer, not an error', async () => {
131
+ await seed()
132
+ const { items, total } = await new KpiMetricValueQuery().kpiMetricValues({ search: 'downtime' }, context())
133
+ expect(total).toBe(1)
134
+ expect(items[0].value).toBe(42)
135
+ })
136
+
137
+ it('finds a value by its date', async () => {
138
+ await seed()
139
+ expect((await new KpiMetricValueQuery().kpiMetricValues({ search: '09-02' }, context())).total).toBe(1)
140
+ })
141
+ })
142
+ })