@uns-kit/core 3.0.47 → 3.0.49
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/MIGRATIONS.md +27 -0
- package/README.md +24 -22
- package/dist/runtime/service-access-examples/README.md +21 -0
- package/dist/runtime/service-access-examples/background-reader/README.md +29 -0
- package/dist/runtime/service-access-examples/background-reader/build.mjs +7 -0
- package/dist/runtime/service-access-examples/background-reader/config-example.json +22 -0
- package/dist/runtime/service-access-examples/background-reader/package.json +37 -0
- package/dist/runtime/service-access-examples/background-reader/src/index.mjs +55 -0
- package/dist/runtime/service-access-examples/background-reader/src/reader.mjs +26 -0
- package/dist/runtime/service-access-examples/catalog-summary/README.md +30 -0
- package/dist/runtime/service-access-examples/catalog-summary/build.mjs +7 -0
- package/dist/runtime/service-access-examples/catalog-summary/config-example.json +23 -0
- package/dist/runtime/service-access-examples/catalog-summary/package.json +92 -0
- package/dist/runtime/service-access-examples/catalog-summary/src/index.mjs +50 -0
- package/dist/runtime/service-access-examples/catalog-summary/src/offer.mjs +35 -0
- package/dist/runtime/service-access-guide.md +259 -112
- package/dist/runtime/service-access-package.d.ts +6 -0
- package/dist/runtime/service-access-package.d.ts.map +1 -0
- package/dist/runtime/service-access-package.js +45 -0
- package/dist/runtime/service-access-package.js.map +1 -0
- package/dist/runtime/service-access-package.test.d.ts +2 -0
- package/dist/runtime/service-access-package.test.d.ts.map +1 -0
- package/dist/runtime/service-access-package.test.js +61 -0
- package/dist/runtime/service-access-package.test.js.map +1 -0
- package/dist/runtime/service-access.d.ts +26 -0
- package/dist/runtime/service-access.d.ts.map +1 -1
- package/dist/runtime/service-access.js +23 -0
- package/dist/runtime/service-access.js.map +1 -1
- package/package.json +1 -1
package/MIGRATIONS.md
CHANGED
|
@@ -9,6 +9,33 @@ Agents must inspect the application's existing ownership and shutdown flow
|
|
|
9
9
|
before editing it. The examples below describe the intended behavior, not a
|
|
10
10
|
mechanical search-and-replace operation.
|
|
11
11
|
|
|
12
|
+
## 3.0.49 - Inline Service access package declarations
|
|
13
|
+
|
|
14
|
+
Existing file declarations remain compatible; no permission is granted by this
|
|
15
|
+
upgrade. New services may put the declaration object directly at
|
|
16
|
+
`package.json.unsDatahub.serviceAccess`. Use `loadServiceAccessDeclaration` from
|
|
17
|
+
`@uns-kit/core/runtime/service-access-package.js` for runtime reads, or the pure
|
|
18
|
+
resolver/schema from `runtime/service-access.js` for host integrations. Both forms
|
|
19
|
+
normalize to the same semantic digest. Do not declare both `serviceAccess` and
|
|
20
|
+
historical `catalogAccess`.
|
|
21
|
+
|
|
22
|
+
Inline metadata requires a Controller advertising `rttServiceAccessInline`;
|
|
23
|
+
Controller 2.2.96 accepts only the file form. Keep the file form until the target
|
|
24
|
+
Controller supports inline declarations. This patch does not implement the planned
|
|
25
|
+
configuration-save approval UI or automatic installation grants. Administrator
|
|
26
|
+
review, caller authorization, MQTT ACLs and rotating token use remain separate.
|
|
27
|
+
Both complete examples now use inline metadata and require the matching host.
|
|
28
|
+
|
|
29
|
+
## 3.0.48 - Checked Service access documentation and examples
|
|
30
|
+
|
|
31
|
+
This patch clarifies existing 3.0.47 contracts; no new permission mode or automatic
|
|
32
|
+
application migration is introduced. Follow the
|
|
33
|
+
[source walkthrough](https://github.com/uns-openhub/uns-kit/blob/master/docs/catalog-publication-access.md)
|
|
34
|
+
or bundled `@uns-kit/core/runtime/service-access-guide.md` and complete projects in
|
|
35
|
+
`dist/runtime/service-access-examples/`. Examples use a rotating token provider;
|
|
36
|
+
background API reads, caller-mediated catalogs and MQTT ACLs are separate.
|
|
37
|
+
An application adopting the capability must still follow the 3.0.47 entry below.
|
|
38
|
+
|
|
12
39
|
## 3.0.47 - Common Service access and publications
|
|
13
40
|
|
|
14
41
|
When crossing `<3.0.47` to `>=3.0.47`, adoption is explicit; existing endpoint authorization and caller forwarding do
|
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ Note: Apps built with uns-kit are intended to be managed by the **UNS OpenHub co
|
|
|
8
8
|
|
|
9
9
|
| Package | Description |
|
|
10
10
|
| --- | --- |
|
|
11
|
-
| [`@uns-kit/core`](https://github.com/uns-openhub/uns-kit/tree/
|
|
12
|
-
| [`@uns-kit/api`](https://github.com/uns-openhub/uns-kit/tree/
|
|
13
|
-
| [`@uns-kit/cron`](https://github.com/uns-openhub/uns-kit/tree/
|
|
14
|
-
| [`@uns-kit/cli`](https://github.com/uns-openhub/uns-kit/tree/
|
|
11
|
+
| [`@uns-kit/core`](https://github.com/uns-openhub/uns-kit/tree/master/packages/uns-core) | Base runtime (UnsProxyProcess, MQTT helpers, config tooling, gRPC gateway). |
|
|
12
|
+
| [`@uns-kit/api`](https://github.com/uns-openhub/uns-kit/tree/master/packages/uns-api) | Express plugin — HTTP endpoints, JWT/JWKS auth, Swagger, UNS metadata. |
|
|
13
|
+
| [`@uns-kit/cron`](https://github.com/uns-openhub/uns-kit/tree/master/packages/uns-cron) | Cron-driven scheduler that emits UNS events on a fixed cadence. |
|
|
14
|
+
| [`@uns-kit/cli`](https://github.com/uns-openhub/uns-kit/tree/master/packages/uns-cli) | CLI for scaffolding new UNS applications. |
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
@@ -95,47 +95,49 @@ const cron = await proc.createCrontabProxy("*/5 * * * *", { event: "tick" });
|
|
|
95
95
|
|
|
96
96
|
## Datahub client (last value + history)
|
|
97
97
|
|
|
98
|
-
`UnsClient` provides a minimal REST client for the UNS OpenHub API, including batch last-value, single-topic catch-all history, and batch range endpoints.
|
|
98
|
+
`UnsClient` provides a minimal REST client for the UNS OpenHub API, including batch last-value, single-topic catch-all history, and batch range endpoints. Use `ServiceTokenProvider` for Controller-managed services so credential rotation is picked up on every request. Declare requirements in `package.json.unsDatahub.serviceAccess` (SDK 3.0.49+ and an inline-capable Controller), or retain the compatible JSON file reference. Review the service's declared scopes and resource paths before Start; never cache a managed token at application startup.
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
Read the [source Service access walkthrough](https://github.com/uns-openhub/uns-kit/blob/master/docs/catalog-publication-access.md)
|
|
101
|
+
or bundled `@uns-kit/core/runtime/service-access-guide.md`. Complete background-reader
|
|
102
|
+
and catalog projects ship in `dist/runtime/service-access-examples/`. A background
|
|
103
|
+
reader uses its service identity only; a catalog operation also carries its caller's
|
|
104
|
+
identity. These API permissions do not create MQTT ACLs or SQL grants.
|
|
103
105
|
|
|
104
|
-
|
|
106
|
+
```ts
|
|
107
|
+
import { ServiceTokenProvider, UnsClient } from "@uns-kit/core";
|
|
105
108
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
});
|
|
109
|
+
const baseUrl = process.env.UNS_CONTROLLER_PUBLIC_BASE;
|
|
110
|
+
if (!baseUrl) throw new Error("Set the trusted Controller base URL.");
|
|
111
|
+
const client = new UnsClient(baseUrl, { tokenProvider: new ServiceTokenProvider() });
|
|
109
112
|
|
|
110
113
|
const values = await client.lastValue([
|
|
111
|
-
"
|
|
112
|
-
"
|
|
114
|
+
"example/plant/line-1/motor/main/temperature",
|
|
115
|
+
"example/plant/line-1/motor/main/status",
|
|
113
116
|
]);
|
|
114
|
-
|
|
117
|
+
// Process the values without logging private records.
|
|
115
118
|
|
|
116
|
-
const data = await client.getAttributeData("
|
|
119
|
+
const data = await client.getAttributeData("example/plant/line-1/motor/main/temperature", {
|
|
117
120
|
from: "2026-05-07T11:17:01.157Z",
|
|
118
121
|
to: "2026-05-07T11:22:01.157Z",
|
|
119
|
-
table: "uns_sij_hrm_furnace_data",
|
|
120
122
|
aggregate: "last",
|
|
121
123
|
dedupe: false,
|
|
122
124
|
});
|
|
123
|
-
|
|
125
|
+
const records = data?.toRecords();
|
|
124
126
|
|
|
125
127
|
const customData = await client.getData("/projects/project-name/path-to-data/data", {
|
|
126
128
|
fromDate: "20260325",
|
|
127
129
|
});
|
|
128
|
-
|
|
130
|
+
const customResult = await customData.json();
|
|
129
131
|
|
|
130
132
|
const batchHistory = await client.history([
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
+
"example/plant/line-1/motor/main/temperature",
|
|
134
|
+
"example/plant/line-1/motor/main/status",
|
|
133
135
|
], {
|
|
134
136
|
from: "2026-04-09T06:00:00Z",
|
|
135
137
|
to: "2026-04-09T07:00:00Z",
|
|
136
138
|
limit: 500,
|
|
137
139
|
});
|
|
138
|
-
|
|
140
|
+
// Process the bounded batch result.
|
|
139
141
|
```
|
|
140
142
|
|
|
141
143
|
## Sub-Asset Publishing
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Service access examples
|
|
2
|
+
|
|
3
|
+
- `background-reader/`: no API or catalog; a reviewed workload token reads one UNS history source.
|
|
4
|
+
- `catalog-summary/`: a catalog-only caller requests a bounded derived count; the workload has no direct source permission.
|
|
5
|
+
|
|
6
|
+
Each folder is an independent, credential-free Node.js package. Copy one folder
|
|
7
|
+
to a new project, install dependencies and build. The declarations are inline in package.json (SDK 3.0.49+). Deploy/review/start
|
|
8
|
+
with a Controller advertising rttServiceAccessInline; Controller 2.2.96 requires
|
|
9
|
+
the compatible file form instead.
|
|
10
|
+
The [walkthrough](https://github.com/uns-openhub/uns-kit/blob/master/docs/catalog-publication-access.md)
|
|
11
|
+
explains what the developer, administrator, Controller, SDK and caller each own.
|
|
12
|
+
|
|
13
|
+
The examples are also shipped in `@uns-kit/core/dist/runtime/service-access-examples/`.
|
|
14
|
+
No real broker, database, user or production endpoint is supplied. Configure your
|
|
15
|
+
own local test resources. These files create no identities, grants or MQTT ACLs.
|
|
16
|
+
|
|
17
|
+
Maintainers: run `pnpm run ts:build` then `pnpm run test:service-access-examples`.
|
|
18
|
+
The verifier packs the SDK, installs it in a temporary consumer, builds/packs both
|
|
19
|
+
examples, and exercises their real handlers/token provider against a synthetic
|
|
20
|
+
loopback API. This checks imports, shipped files, token rotation, denial and
|
|
21
|
+
closed output, not real Controller policy, MQTT connectivity or cluster ownership.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# background-reader
|
|
2
|
+
|
|
3
|
+
A complete Node.js 22+ example using SDK APIs from 3.0.49 onward.
|
|
4
|
+
Read the [Service access walkthrough](https://github.com/uns-openhub/uns-kit/blob/master/docs/catalog-publication-access.md) for the declaration, Controller review and acceptance steps.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
pnpm install
|
|
8
|
+
pnpm run build
|
|
9
|
+
cp config-example.json config.json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The example configuration targets a local development broker/Controller. Select
|
|
13
|
+
your own endpoints and secret references before deployment. The Controller owns
|
|
14
|
+
instance configuration when deployed. `MQTT_USERNAME`/`MQTT_PASSWORD` concern broker
|
|
15
|
+
ACLs only and grant no OpenHub API rights. Do not commit `config.json` or `.env`.
|
|
16
|
+
|
|
17
|
+
Deploy the package, apply its exact-version Service access policy, and then Start.
|
|
18
|
+
The Controller supplies managed identity/credential paths. Do not manufacture RTT
|
|
19
|
+
launch variables or paste a human token into configuration.
|
|
20
|
+
|
|
21
|
+
This worker has no API or catalog. It polls five minutes of history, at most
|
|
22
|
+
100 rows, every 30 seconds while active. Provision the exact declared source in
|
|
23
|
+
your own test environment; zero rows is a valid result. Its token can read that
|
|
24
|
+
source, while another source must return 403. SIGINT/SIGTERM aborts the current
|
|
25
|
+
request and stops polling before runtime cleanup.
|
|
26
|
+
|
|
27
|
+
The access declaration is inline in package.json. This representation requires
|
|
28
|
+
Controller capability `rttServiceAccessInline`; it is unsupported by 2.2.96.
|
|
29
|
+
The SDK also accepts a package-relative JSON file reference for older hosts.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { copyFile, mkdir, rm } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
await rm("dist", { recursive: true, force: true });
|
|
4
|
+
await mkdir("dist");
|
|
5
|
+
await copyFile("src/index.mjs", "dist/index.js");
|
|
6
|
+
await copyFile("src/reader.mjs", "dist/reader.mjs");
|
|
7
|
+
// service-access.json stays at the package root, as declared in package.json.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"infra": {
|
|
3
|
+
"host": "localhost"
|
|
4
|
+
},
|
|
5
|
+
"uns": {
|
|
6
|
+
"processName": "example-background-reader",
|
|
7
|
+
"instanceMode": "wait",
|
|
8
|
+
"handover": false
|
|
9
|
+
},
|
|
10
|
+
"mqtt": {
|
|
11
|
+
"username": {
|
|
12
|
+
"provider": "env",
|
|
13
|
+
"key": "MQTT_USERNAME",
|
|
14
|
+
"default": ""
|
|
15
|
+
},
|
|
16
|
+
"password": {
|
|
17
|
+
"provider": "env",
|
|
18
|
+
"key": "MQTT_PASSWORD",
|
|
19
|
+
"default": ""
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "example-background-reader",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"config-example.json"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "node build.mjs",
|
|
16
|
+
"start": "node dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@uns-kit/core": "^3.0.49"
|
|
20
|
+
},
|
|
21
|
+
"unsDatahub": {
|
|
22
|
+
"schemaVersion": 1,
|
|
23
|
+
"kind": "addon",
|
|
24
|
+
"controllerCompatibility": ">=2 <3",
|
|
25
|
+
"serviceAccess": {
|
|
26
|
+
"protocol": 1,
|
|
27
|
+
"permissions": {
|
|
28
|
+
"scopes": [
|
|
29
|
+
"read:uns"
|
|
30
|
+
],
|
|
31
|
+
"accessRules": [
|
|
32
|
+
"example/background/line-1/equipment/main/temperature"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ConfigFile, UnsProxyProcess } from "@uns-kit/core";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
|
|
4
|
+
import { readHistory } from "./reader.mjs";
|
|
5
|
+
|
|
6
|
+
const config = await ConfigFile.loadConfig();
|
|
7
|
+
const controllerBase = process.env.UNS_CONTROLLER_PUBLIC_BASE;
|
|
8
|
+
if (!controllerBase) throw new Error("UNS_CONTROLLER_PUBLIC_BASE must name your Controller.");
|
|
9
|
+
const stop = new AbortController();
|
|
10
|
+
const runtime = new UnsProxyProcess(config.infra.host, {
|
|
11
|
+
processName: config.uns.processName,
|
|
12
|
+
username: config.mqtt?.username,
|
|
13
|
+
password: config.mqtt?.password,
|
|
14
|
+
});
|
|
15
|
+
let currentRead = Promise.resolve();
|
|
16
|
+
let stopping;
|
|
17
|
+
const shutdown = () =>
|
|
18
|
+
(stopping ??= (async () => {
|
|
19
|
+
stop.abort();
|
|
20
|
+
await currentRead.catch(() => {});
|
|
21
|
+
await runtime.shutdown();
|
|
22
|
+
})());
|
|
23
|
+
for (const signal of ["SIGINT", "SIGTERM"])
|
|
24
|
+
process.once(signal, () => {
|
|
25
|
+
void shutdown().then(
|
|
26
|
+
() => process.exit(0),
|
|
27
|
+
() => {
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
try {
|
|
33
|
+
await runtime.prepareHandover(config.uns.instanceMode, config.uns.handover);
|
|
34
|
+
while (!stop.signal.aborted) {
|
|
35
|
+
if (runtime.isActive()) {
|
|
36
|
+
const to = new Date(),
|
|
37
|
+
from = new Date(to.getTime() - 5 * 60000);
|
|
38
|
+
try {
|
|
39
|
+
currentRead = readHistory(controllerBase, from.toISOString(), to.toISOString(), { signal: stop.signal });
|
|
40
|
+
const rows = await currentRead;
|
|
41
|
+
// Put application processing here. Never log bearer values or raw records.
|
|
42
|
+
console.info(`Read ${rows.length} samples.`);
|
|
43
|
+
} catch {
|
|
44
|
+
if (!stop.signal.aborted) console.warn("History read failed; check Service access and API availability.");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
await delay(30000, undefined, { signal: stop.signal });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (!stop.signal.aborted) throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} finally {
|
|
54
|
+
await shutdown();
|
|
55
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ServiceTokenProvider } from "@uns-kit/core/tools/auth/service-token-provider.js";
|
|
2
|
+
import { loadServiceAccessDeclaration } from "@uns-kit/core/runtime/service-access-package.js";
|
|
3
|
+
|
|
4
|
+
const declaration = await loadServiceAccessDeclaration(new URL("../package.json", import.meta.url));
|
|
5
|
+
if (declaration?.permissions.scopes.length !== 1 || declaration.permissions.scopes[0] !== "read:uns" ||
|
|
6
|
+
declaration.permissions.accessRules.length !== 1 || /[+#]/.test(declaration.permissions.accessRules[0]))
|
|
7
|
+
throw new Error("This example requires one exact declared background source.");
|
|
8
|
+
export const sourceTopic = declaration.permissions.accessRules[0];
|
|
9
|
+
|
|
10
|
+
/** A background API read has a workload identity and no human caller token. */
|
|
11
|
+
export async function readHistory(controllerBase, from, to, { tokenProvider = new ServiceTokenProvider(), fetch: request = fetch, signal } = {}) {
|
|
12
|
+
const token = await tokenProvider.getAccessToken();
|
|
13
|
+
if (!token) throw new Error("Service credential unavailable; review Service access.");
|
|
14
|
+
const response = await request(new URL("/api/catchall/batch/range", controllerBase), {
|
|
15
|
+
method: "POST",
|
|
16
|
+
redirect: "error",
|
|
17
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
18
|
+
body: JSON.stringify({ topics: [sourceTopic], from, to, limit: 100 }),
|
|
19
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(10000)]) : AbortSignal.timeout(10000),
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) throw new Error(`UNS history returned HTTP ${response.status}; no credential fallback.`);
|
|
22
|
+
const result = await response.json();
|
|
23
|
+
const rows = result.results?.[0]?.data;
|
|
24
|
+
if (!Array.isArray(rows)) throw new Error("Unexpected history response.");
|
|
25
|
+
return rows;
|
|
26
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# catalog-summary
|
|
2
|
+
|
|
3
|
+
A complete Node.js 22+ example using SDK APIs from 3.0.49 onward.
|
|
4
|
+
Read the [Service access walkthrough](https://github.com/uns-openhub/uns-kit/blob/master/docs/catalog-publication-access.md) for the declaration, Controller review and acceptance steps.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
pnpm install
|
|
8
|
+
pnpm run build
|
|
9
|
+
cp config-example.json config.json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The example configuration targets a local development broker/Controller. Select
|
|
13
|
+
your own endpoints and secret references before deployment. The Controller owns
|
|
14
|
+
instance configuration when deployed. `MQTT_USERNAME`/`MQTT_PASSWORD` concern broker
|
|
15
|
+
ACLs only and grant no OpenHub API rights. Do not commit `config.json` or `.env`.
|
|
16
|
+
|
|
17
|
+
Deploy the package, apply its exact-version Service access policy, and then Start.
|
|
18
|
+
The Controller supplies managed identity/credential paths. Do not manufacture RTT
|
|
19
|
+
launch variables or paste a human token into configuration.
|
|
20
|
+
|
|
21
|
+
This service requires a managed Controller launch for signed catalog route
|
|
22
|
+
publication. Its workload has no direct UNS access. Apply catalog:sources:read
|
|
23
|
+
and approve the publication for the intended audience. Give a test reader access
|
|
24
|
+
to the operation path, not the private source path. Execute a timezone-qualified
|
|
25
|
+
window of at most one hour. Expect {count, hasMore}; raw source fields are excluded.
|
|
26
|
+
A direct source request with the reader or service token must return 403.
|
|
27
|
+
|
|
28
|
+
The access declaration is inline in package.json. This representation requires
|
|
29
|
+
Controller capability `rttServiceAccessInline`; it is unsupported by 2.2.96.
|
|
30
|
+
The SDK also accepts a package-relative JSON file reference for older hosts.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { copyFile, mkdir, rm } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
await rm("dist", { recursive: true, force: true });
|
|
4
|
+
await mkdir("dist");
|
|
5
|
+
await copyFile("src/index.mjs", "dist/index.js");
|
|
6
|
+
await copyFile("src/offer.mjs", "dist/offer.mjs");
|
|
7
|
+
// The declaration lives in package.json, which npm always retains.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"infra": {
|
|
3
|
+
"host": "localhost"
|
|
4
|
+
},
|
|
5
|
+
"uns": {
|
|
6
|
+
"processName": "example-catalog-summary",
|
|
7
|
+
"instanceMode": "wait",
|
|
8
|
+
"handover": false,
|
|
9
|
+
"jwksWellKnownUrl": "http://localhost:3200/api/.well-known/jwks.json"
|
|
10
|
+
},
|
|
11
|
+
"mqtt": {
|
|
12
|
+
"username": {
|
|
13
|
+
"provider": "env",
|
|
14
|
+
"key": "MQTT_USERNAME",
|
|
15
|
+
"default": ""
|
|
16
|
+
},
|
|
17
|
+
"password": {
|
|
18
|
+
"provider": "env",
|
|
19
|
+
"key": "MQTT_PASSWORD",
|
|
20
|
+
"default": ""
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "example-catalog-summary",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"config-example.json"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "node build.mjs",
|
|
16
|
+
"start": "node dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@uns-kit/core": "^3.0.49",
|
|
20
|
+
"@uns-kit/api": "^3.0.49"
|
|
21
|
+
},
|
|
22
|
+
"unsDatahub": {
|
|
23
|
+
"schemaVersion": 1,
|
|
24
|
+
"kind": "addon",
|
|
25
|
+
"controllerCompatibility": ">=2 <3",
|
|
26
|
+
"serviceAccess": {
|
|
27
|
+
"protocol": 1,
|
|
28
|
+
"permissions": {
|
|
29
|
+
"scopes": [],
|
|
30
|
+
"accessRules": []
|
|
31
|
+
},
|
|
32
|
+
"publications": {
|
|
33
|
+
"protocol": 1,
|
|
34
|
+
"operations": [
|
|
35
|
+
{
|
|
36
|
+
"id": "sample-count",
|
|
37
|
+
"title": "Temperature sample count",
|
|
38
|
+
"method": "GET",
|
|
39
|
+
"path": "/api/example/public/line-1/equipment/dataset/sample-count/json",
|
|
40
|
+
"mode": "approved-service",
|
|
41
|
+
"requestSchema": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"required": [
|
|
45
|
+
"from",
|
|
46
|
+
"to"
|
|
47
|
+
],
|
|
48
|
+
"properties": {
|
|
49
|
+
"from": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"format": "date-time"
|
|
52
|
+
},
|
|
53
|
+
"to": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"format": "date-time"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"responseSchema": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"additionalProperties": false,
|
|
62
|
+
"required": [
|
|
63
|
+
"count",
|
|
64
|
+
"hasMore"
|
|
65
|
+
],
|
|
66
|
+
"properties": {
|
|
67
|
+
"count": {
|
|
68
|
+
"type": "integer",
|
|
69
|
+
"minimum": 0
|
|
70
|
+
},
|
|
71
|
+
"hasMore": {
|
|
72
|
+
"type": "boolean"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"sources": [
|
|
77
|
+
{
|
|
78
|
+
"id": "temperature",
|
|
79
|
+
"kind": "history",
|
|
80
|
+
"topic": "example/private/line-1/equipment/main/temperature",
|
|
81
|
+
"maxWindowMs": 3600000,
|
|
82
|
+
"maxRows": 1000
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"timeoutMs": 10000,
|
|
86
|
+
"maxResponseBytes": 4096
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import "@uns-kit/api";
|
|
2
|
+
import { registerApiCatalog } from "@uns-kit/api";
|
|
3
|
+
import { ConfigFile, UnsProxyProcess } from "@uns-kit/core";
|
|
4
|
+
import { CatalogPublicationClient } from "@uns-kit/core/runtime/catalog-publication-client.js";
|
|
5
|
+
import { loadServiceAccessDeclaration } from "@uns-kit/core/runtime/service-access-package.js";
|
|
6
|
+
|
|
7
|
+
import { createSummaryOffer } from "./offer.mjs";
|
|
8
|
+
|
|
9
|
+
const required = (key) => {
|
|
10
|
+
const value = process.env[key];
|
|
11
|
+
if (!value) throw new Error(`Missing managed runtime value: ${key}`);
|
|
12
|
+
return value;
|
|
13
|
+
};
|
|
14
|
+
const declaration = await loadServiceAccessDeclaration(new URL("../package.json", import.meta.url));
|
|
15
|
+
if (!declaration?.publications) throw new Error("Package must declare its catalog operations.");
|
|
16
|
+
const client = new CatalogPublicationClient({
|
|
17
|
+
controllerBase: required("UNS_CONTROLLER_PUBLIC_BASE"),
|
|
18
|
+
rttNode: required("RTT_NODE"),
|
|
19
|
+
instanceId: required("RTT_INSTANCE_ID"),
|
|
20
|
+
version: required("version"),
|
|
21
|
+
manifest: declaration.publications,
|
|
22
|
+
});
|
|
23
|
+
const config = await ConfigFile.loadConfig();
|
|
24
|
+
if (!config.uns.jwksWellKnownUrl) throw new Error("Configure the Controller JWKS URL.");
|
|
25
|
+
const runtime = new UnsProxyProcess(config.infra.host, {
|
|
26
|
+
processName: config.uns.processName,
|
|
27
|
+
username: config.mqtt?.username,
|
|
28
|
+
password: config.mqtt?.password,
|
|
29
|
+
});
|
|
30
|
+
let stopping;
|
|
31
|
+
const shutdown = () => (stopping ??= runtime.shutdown());
|
|
32
|
+
for (const signal of ["SIGINT", "SIGTERM"])
|
|
33
|
+
process.once(signal, () => {
|
|
34
|
+
void shutdown().then(
|
|
35
|
+
() => process.exit(0),
|
|
36
|
+
() => {
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
await runtime.prepareHandover(config.uns.instanceMode, config.uns.handover);
|
|
43
|
+
const api = await runtime.createApiProxy("catalog", { jwks: { wellKnownJwksUrl: config.uns.jwksWellKnownUrl } });
|
|
44
|
+
api.registerCatalogRoute();
|
|
45
|
+
await registerApiCatalog(api, { dataOfferSources: { sampleCount: createSummaryOffer(client) }, context: undefined });
|
|
46
|
+
} catch {
|
|
47
|
+
console.error("Catalog startup failed; check configuration, Service access and runtime logs.");
|
|
48
|
+
await shutdown();
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineDataCatalogField, defineDataCatalogOfferSource, defineDataCatalogQueryParam, defineDataCatalogSchema } from "@uns-kit/api";
|
|
2
|
+
import { catalogPublicationHandler } from "@uns-kit/api/catalog-publication-handler.js";
|
|
3
|
+
|
|
4
|
+
export function createSummaryOffer(client) {
|
|
5
|
+
return defineDataCatalogOfferSource({
|
|
6
|
+
offerId: "sample-count",
|
|
7
|
+
displayName: "Temperature sample count",
|
|
8
|
+
description: "Count of samples in an approved interval; raw measurements are not returned.",
|
|
9
|
+
topic: "example/public/line-1",
|
|
10
|
+
asset: "equipment",
|
|
11
|
+
objectType: "dataset",
|
|
12
|
+
objectId: "sample-count",
|
|
13
|
+
attribute: "json",
|
|
14
|
+
method: "GET",
|
|
15
|
+
queryParams: ["from", "to"].map((name) =>
|
|
16
|
+
defineDataCatalogQueryParam(name, "ISO 8601 with timezone; at most one hour.", { required: true, format: "date-time" }),
|
|
17
|
+
),
|
|
18
|
+
response: { statusCode: "200", contentType: "application/json" },
|
|
19
|
+
schema: defineDataCatalogSchema({
|
|
20
|
+
id: "sample-count",
|
|
21
|
+
title: "Sample count",
|
|
22
|
+
contentType: "application/json",
|
|
23
|
+
fields: [
|
|
24
|
+
defineDataCatalogField("count", "integer", "Number of returned samples"),
|
|
25
|
+
defineDataCatalogField("hasMore", "boolean", "More source samples exist than the bounded result contains"),
|
|
26
|
+
],
|
|
27
|
+
}),
|
|
28
|
+
handler: ({ req, res }) =>
|
|
29
|
+
catalogPublicationHandler(client, "sample-count", async ({ history }) => {
|
|
30
|
+
const rows = await history("temperature");
|
|
31
|
+
if (!Array.isArray(rows.data) || typeof rows.truncated !== "boolean") throw new Error("Unexpected source response.");
|
|
32
|
+
return { count: rows.data.length, hasMore: rows.truncated };
|
|
33
|
+
})(req, res),
|
|
34
|
+
});
|
|
35
|
+
}
|