diff --git a/package-lock.json b/package-lock.json index 6691fe0..f36ecf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2935,6 +2935,10 @@ "resolved": "packages/plugin-dkg-publisher", "link": true }, + "node_modules/@dkg/plugin-epcis": { + "resolved": "packages/plugin-epcis", + "link": true + }, "node_modules/@dkg/plugin-example": { "resolved": "packages/plugin-example", "link": true @@ -9195,6 +9199,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/an-array": { "version": "1.0.0", "license": "MIT", @@ -14718,6 +14761,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fast-xml-parser": { "version": "4.5.3", "funding": [ @@ -28248,6 +28307,43 @@ "uuid": "dist/bin/uuid" } }, + "packages/plugin-epcis": { + "name": "@dkg/plugin-epcis", + "version": "0.0.1", + "dependencies": { + "@dkg/plugin-swagger": "^0.0.2", + "@dkg/plugins": "^0.0.2", + "ajv": "^8.17.0", + "ajv-formats": "^3.0.0" + }, + "devDependencies": { + "@dkg/eslint-config": "*", + "@dkg/typescript-config": "*", + "tsup": "^8.5.0" + } + }, + "packages/plugin-epcis/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "packages/plugin-epcis/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "packages/plugin-example": { "name": "@dkg/plugin-example", "version": "0.0.3", diff --git a/packages/plugin-epcis/docs/EPCIS-Integration-Guide.md b/packages/plugin-epcis/docs/EPCIS-Integration-Guide.md new file mode 100644 index 0000000..63aef1c --- /dev/null +++ b/packages/plugin-epcis/docs/EPCIS-Integration-Guide.md @@ -0,0 +1,612 @@ +# πŸ“˜ EPCIS-DKG Integration Guide + +## Table of Contents + +1. [Overview & Architecture](#1-overview--architecture) +2. [Quick Start](#2-quick-start) +3. [EPCIS Event Types Explained](#3-epcis-event-types-explained) +4. [API Reference](#4-api-reference) +5. [Data Flow & DKG Publishing](#5-data-flow--dkg-publishing) +6. [Query Examples](#6-query-examples) +7. [Troubleshooting](#7-troubleshooting) + +--- + +## 1. Overview & Architecture + +### What This System Does + +This integration bridges **GS1 EPCIS 2.0** (Electronic Product Code Information Services) with the **OriginTrail Decentralized Knowledge Graph (DKG)**. It allows you to: + +- **Capture** supply chain events in standard EPCIS format +- **Publish** them as tamper-proof Knowledge Assets on the DKG +- **Query** events using semantic filters across the distributed network + +### Why Use DKG for EPCIS? + +| Traditional EPCIS | EPCIS + DKG | +|-------------------|-------------| +| Centralized database | Decentralized, permissionless network | +| Single point of failure | Replicated across multiple nodes | +| Trust the provider | Cryptographically verifiable | +| Siloed data | Interlinked Knowledge Graph | +| Company-controlled | Owned via blockchain (UAL) | + +### Architecture Overview + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Your Application β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ HTTP POST /epcis/capture + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ EPCIS Plugin β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Validation │───▢│ JSON-LD Transform β”‚ β”‚ +β”‚ β”‚ (GS1 Schema) β”‚ β”‚ (EPCIS Context) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ DKG Publisher Plugin β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Asset Queue │───▢│ BullMQ │───▢│ DKG Network β”‚ β”‚ +β”‚ β”‚ (MySQL) β”‚ β”‚ Workers β”‚ β”‚ (via dkg.js) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ OriginTrail Decentralized Knowledge Graph β”‚ +β”‚ β”‚ +β”‚ Knowledge Asset (UAL: did:dkg:otp/0x.../123456) β”‚ +β”‚ β”œβ”€β”€ EPCIS Event Data (RDF/JSON-LD) β”‚ +β”‚ β”œβ”€β”€ Cryptographic Proof (Blockchain anchored) β”‚ +β”‚ └── Ownership (NFT) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## 2. Quick Start + +### Prerequisites + +- DKG Node running (with EPCIS and Publisher plugins enabled) +- Access to the API endpoint (default: `http://localhost:9200`) + +### Step 1: Send Your First EPCIS Event + +```bash +curl -X POST http://localhost:9200/epcis/capture \ + -H "Content-Type: application/json" \ + -d '{ + "@context": { + "@vocab": "https://gs1.github.io/EPCIS/", + "epcis": "https://gs1.github.io/EPCIS/", + "cbv": "https://ref.gs1.org/cbv/", + "type": "@type", + "id": "@id", + "epcisBody": "epcis:epcisBody", + "eventList": "epcis:eventList" + }, + "type": "EPCISDocument", + "schemaVersion": "2.0", + "creationDate": "2024-01-01T00:00:00Z", + "epcisBody": { + "eventList": [{ + "type": "ObjectEvent", + "eventTime": "2024-01-01T00:00:00.000Z", + "eventTimeZoneOffset": "+00:00", + "epcList": ["urn:epc:id:sgtin:0614141.107346.2017"], + "action": "OBSERVE", + "bizStep": "https://ref.gs1.org/cbv/BizStep-receiving", + "disposition": "https://ref.gs1.org/cbv/Disp-in_progress", + "readPoint": {"id": "urn:epc:id:sgln:0614141.00001.0"}, + "bizLocation": {"id": "urn:epc:id:sgln:0614141.00001.0"} + }] + } + }' +``` + +### Step 2: Check Status + +The response includes a `captureID`. Use it to check publishing status: + +```bash +curl http://localhost:9200/epcis/capture/123 +``` + +Possible statuses: + +- `queued` - Waiting to be published +- `processing` - Currently being published to DKG +- `published` - Successfully published (includes UAL) +- `failed` - Publishing failed (includes error message) + +### Step 3: Query Events + +Once published, query events from the DKG: + +```bash +# By EPC +curl "http://localhost:9200/epcis/events?epc=urn:epc:id:sgtin:0614141.107346.2017" + +# By time range +curl "http://localhost:9200/epcis/events?from=2024-01-01T00:00:00Z&to=2024-12-31T23:59:59Z" + +# By business step +curl "http://localhost:9200/epcis/events?bizStep=inspecting" +``` + +> πŸ’‘ **Interactive Documentation**: For detailed request/response schemas and to test the API live, visit the Swagger UI at `/swagger` + +--- + +## 3. EPCIS Event Types Explained + +### What is EPCIS? + +EPCIS (Electronic Product Code Information Services) is a GS1 standard for capturing and sharing supply chain events. It answers the "what, where, when, and why" of products moving through a supply chain. + +### The Five Event Types + +| Event Type | Purpose | Example Use Case | +|------------|---------|------------------| +| **ObjectEvent** | Track individual items | Product inspection, quality check | +| **AggregationEvent** | Items grouped/ungrouped | Packing items into a case | +| **TransactionEvent** | Business transactions | Purchase order, invoice | +| **TransformationEvent** | Inputβ†’Output conversion | Manufacturing, assembly | +| **AssociationEvent** | Link assets together | Sensor attached to container | + +### Action Types + +- **ADD** - New item introduced (e.g., manufactured, received) +- **OBSERVE** - Item observed without state change (e.g., scanned at checkpoint) +- **DELETE** - Item removed from tracking (e.g., sold, destroyed) + +### Business Steps (bizStep) + +Common GS1 CBV (Core Business Vocabulary) business steps: + +| bizStep | Description | +|---------|-------------| +| `receiving` | Goods received at a location | +| `shipping` | Goods shipped from a location | +| `inspecting` | Quality inspection performed | +| `assembling` | Components assembled into product | +| `packing` | Items packed for shipment | +| `commissioning` | New serial assigned (e.g., manufacturing) | +| `decommissioning` | Serial number retired | + +> **Shorthand supported**: You can use just `"assembling"` instead of the full URI `"https://ref.gs1.org/cbv/BizStep-assembling"` + +--- + +## 4. API Reference + +### Understanding the JSON-LD Context + +EPCIS documents use JSON-LD (Linked Data) format. The `@context` object maps terms to URIs for proper semantic interpretation: + +```json +{ + "@context": { + "@vocab": "https://gs1.github.io/EPCIS/", + "epcis": "https://gs1.github.io/EPCIS/", + "cbv": "https://ref.gs1.org/cbv/", + "type": "@type", + "id": "@id", + "epcisBody": "epcis:epcisBody", + "eventList": "epcis:eventList" + } +} +``` + +| Key | Purpose | +|-----|---------| +| `@vocab` | Default namespace for unmapped terms | +| `epcis` | EPCIS vocabulary namespace | +| `cbv` | GS1 Core Business Vocabulary | +| `type` / `id` | Maps to JSON-LD keywords | +| `epcisBody`, `eventList` | Explicit term mappings | + +> **Note**: You can also use the shorthand `["https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld"]` but the explicit context above gives you more control and is properly tested. + +--- + +### POST `/epcis/capture` + +Accept an EPCIS Document and queue it for publishing to DKG. + +**Request Body**: EPCISDocument (JSON-LD) + +```json +{ + "@context": { + "@vocab": "https://gs1.github.io/EPCIS/", + "epcis": "https://gs1.github.io/EPCIS/", + "cbv": "https://ref.gs1.org/cbv/", + "type": "@type", + "id": "@id", + "epcisBody": "epcis:epcisBody", + "eventList": "epcis:eventList" + }, + "type": "EPCISDocument", + "schemaVersion": "2.0", + "creationDate": "2024-01-01T00:00:00Z", + "epcisBody": { + "eventList": [/* array of events */] + } +} +``` + +**Response** (HTTP 202 Accepted): + +```json +{ + "status": "202", + "receivedAt": "2024-01-01T00:00:01.123Z", + "captureID": "456", + "eventCount": 1 +} +``` + +--- + +### GET `/epcis/capture/:captureID` + +Check the status of a previously submitted capture. + +**Response**: + +```json +{ + "status": "published", + "captureID": "456", + "UAL": "did:dkg:otp/0x1234.../789", + "publishedAt": "2024-01-01T00:01:23.456Z" +} +``` + +| Field | Description | +|-------|-------------| +| `status` | `queued` / `processing` / `published` / `failed` | +| `UAL` | Uniform Asset Locator (only when published) | +| `error` | Error message (only when failed) | + +--- + +### GET `/epcis/events` + +Query EPCIS events from the DKG. + +**Query Parameters**: + +| Parameter | Type | Description | Example | +|-----------|------|-------------|---------| +| `epc` | string | Filter by EPC identifier | `urn:epc:id:sgtin:0614141.107346.2017` | +| `from` | string (ISO 8601) | Start of time range | `2024-01-01T00:00:00Z` | +| `to` | string (ISO 8601) | End of time range | `2024-12-31T23:59:59Z` | +| `bizStep` | string | Filter by business step | `assembling` or full URI | +| `bizLocation` | string | Filter by location | `urn:epc:id:sgln:0614141.00001.0` | +| `ual` | string | Get specific event by UAL | `did:dkg:otp/...` | + +**Response**: + +```json +{ + "success": true, + "query": "SELECT ...", + "results": [/* array of matching events */], + "count": 5 +} +``` + +--- + +## 5. Data Flow & DKG Publishing + +### Publishing Pipeline + +``` +1. CAPTURE REQUEST + └─▢ Validate against GS1 EPCIS 2.0 JSON Schema + +2. QUEUE (Tier 1 - MySQL) + └─▢ Asset registered with status "queued" + └─▢ Assigned priority and metadata + +3. POLLING (every 2 seconds) + └─▢ QueuePoller checks for available wallets + └─▢ Moves jobs to BullMQ (Tier 2 - Redis) + +4. PROCESSING (BullMQ Workers) + └─▢ Worker acquires wallet lock + └─▢ Wraps content as JSON-LD Knowledge Asset + └─▢ Calls dkg.js asset.create() + +5. DKG NETWORK + └─▢ Content replicated to DKG nodes + └─▢ Cryptographic proof anchored to blockchain + └─▢ UAL (NFT) minted for ownership + +6. COMPLETION + └─▢ Asset status updated to "published" + └─▢ UAL stored for future queries +``` + +### What is a UAL? + +A **Uniform Asset Locator** is a globally unique identifier for your Knowledge Asset: + +``` +did:dkg:otp/0x1234567890abcdef/123456 +β””β”€β”€β”¬β”€β”€β”˜ β””β”¬β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”¬β”€β”€β”˜ + β”‚ β”‚ β”‚ β”‚ + β”‚ β”‚ β”‚ └── Asset ID + β”‚ β”‚ └── Contract address + β”‚ └── Blockchain (otp = OriginTrail Parachain) + └── DID method +``` + +With a UAL, you can: + +- **Verify** the content hasn't been tampered with +- **Prove** ownership on the blockchain +- **Query** the event data from any DKG node +- **Link** to other Knowledge Assets + +--- + +## 6. Query Examples + +### Find All Events for a Product + +```bash +curl "http://localhost:9200/epcis/events?epc=urn:epc:id:sgtin:0614141.107346.2017" +``` + +### Find Assembly Events at a Specific Location + +```bash +curl "http://localhost:9200/epcis/events?bizStep=assembling&bizLocation=urn:epc:id:sgln:0614141.00001.0" +``` + +### Get Full Event Details by UAL + +```bash +curl "http://localhost:9200/epcis/events?ual=did:dkg:otp/0x1234.../789" +``` + +### Time Range Query + +```bash +curl "http://localhost:9200/epcis/events?from=2024-01-01T00:00:00Z&to=2024-01-31T23:59:59Z" +``` + +### SPARQL Direct Query + +Under the hood, queries are translated to SPARQL. Example generated query: + +```sparql +PREFIX epcis: +PREFIX schema: + +SELECT ?ual ?eventType ?eventTime ?epc ?bizStep ?disposition ?readPoint ?bizLocation +WHERE { + GRAPH ?ual { + ?event a ?eventType . + ?event epcis:epcList "urn:epc:id:sgtin:0614141.107346.2017" . + OPTIONAL { ?event epcis:bizStep ?bizStep . } + OPTIONAL { ?event epcis:eventTime ?eventTime . } + } + FILTER(STRSTARTS(STR(?eventType), "https://gs1.github.io/EPCIS/")) +} +ORDER BY DESC(?eventTime) +LIMIT 100 +``` + +--- + +## 7. Troubleshooting + +### Common Errors + +| Error | Cause | Solution | +|-------|-------|----------| +| `Invalid EPCISDocument` | Schema validation failed | Check your JSON matches EPCIS 2.0 spec | +| `Invalid captureID format` | Non-numeric captureID | Use the numeric ID from capture response | +| `Capture not found` | Unknown captureID | Verify the ID; it may have been deleted | +| `Publishing failed` | DKG network error | Check wallet balance, node connectivity | +| `No available wallets` | All wallets are busy | Wait or add more wallets to the pool | + +### Checking System Health + +**Publisher Dashboard**: Visit `/admin/queues` to see: + +- Active jobs +- Waiting queue +- Failed jobs with error details +- Worker status + +**API Health**: The Swagger UI at `/swagger` shows all available endpoints and their status. + +### Validation Errors + +The system validates against the official GS1 EPCIS 2.0 JSON Schema. Common issues: + +1. **Missing `@context`** - Must include EPCIS context +2. **Invalid `eventTime`** - Must be ISO 8601 format +3. **Wrong `type`** - Must be exactly `"EPCISDocument"` (case-sensitive) +4. **Invalid `bizStep`** - Must be valid CBV URI or shorthand + +### Getting Help + +- **Swagger UI**: `http://your-server/swagger` - Interactive API docs +- **OpenAPI Spec**: `http://your-server/openapi` - Raw JSON spec +- **Logs**: Check server logs for detailed error messages + +--- + +## Appendix: Sample EPCIS Documents + +### Object Event (Receiving Goods) + +```json +{ + "@context": { + "@vocab": "https://gs1.github.io/EPCIS/", + "epcis": "https://gs1.github.io/EPCIS/", + "cbv": "https://ref.gs1.org/cbv/", + "type": "@type", + "id": "@id", + "epcisBody": "epcis:epcisBody", + "eventList": "epcis:eventList" + }, + "type": "EPCISDocument", + "schemaVersion": "2.0", + "creationDate": "2024-01-01T00:00:00Z", + "epcisBody": { + "eventList": [{ + "type": "ObjectEvent", + "eventTime": "2024-01-01T00:00:00.000Z", + "eventTimeZoneOffset": "+00:00", + "epcList": ["urn:epc:id:sgtin:0614141.107346.2017"], + "action": "OBSERVE", + "bizStep": "https://ref.gs1.org/cbv/BizStep-receiving", + "disposition": "https://ref.gs1.org/cbv/Disp-in_progress", + "readPoint": {"id": "urn:epc:id:sgln:0614141.00001.0"}, + "bizLocation": {"id": "urn:epc:id:sgln:0614141.00001.0"}, + "bizTransactionList": [ + { + "type": "urn:epcglobal:cbv:btt:po", + "bizTransaction": "urn:epc:id:gdti:0614141.00001.1234" + } + ] + }] + } +} +``` + +### Transformation Event (Assembly) + +```json +{ + "@context": { + "@vocab": "https://gs1.github.io/EPCIS/", + "epcis": "https://gs1.github.io/EPCIS/", + "cbv": "https://ref.gs1.org/cbv/", + "type": "@type", + "id": "@id", + "epcisBody": "epcis:epcisBody", + "eventList": "epcis:eventList" + }, + "type": "EPCISDocument", + "schemaVersion": "2.0", + "creationDate": "2024-01-01T00:00:00Z", + "epcisBody": { + "eventList": [{ + "type": "TransformationEvent", + "eventTime": "2024-01-01T12:00:00.000Z", + "eventTimeZoneOffset": "+00:00", + "inputEPCList": [ + "urn:epc:id:sgtin:0614141.107346.001", + "urn:epc:id:sgtin:0614141.107346.002" + ], + "outputEPCList": [ + "urn:epc:id:sgtin:0614141.107347.001" + ], + "bizStep": "https://ref.gs1.org/cbv/BizStep-assembling", + "bizLocation": {"id": "urn:epc:id:sgln:0614141.00002.0"} + }] + } +} +``` + +### Aggregation Event (Packing) + +```json +{ + "@context": { + "@vocab": "https://gs1.github.io/EPCIS/", + "epcis": "https://gs1.github.io/EPCIS/", + "cbv": "https://ref.gs1.org/cbv/", + "type": "@type", + "id": "@id", + "epcisBody": "epcis:epcisBody", + "eventList": "epcis:eventList" + }, + "type": "EPCISDocument", + "schemaVersion": "2.0", + "creationDate": "2024-01-01T00:00:00Z", + "epcisBody": { + "eventList": [{ + "type": "AggregationEvent", + "eventTime": "2024-01-01T14:00:00.000Z", + "eventTimeZoneOffset": "+00:00", + "parentID": "urn:epc:id:sscc:0614141.0000000001", + "childEPCs": [ + "urn:epc:id:sgtin:0614141.107346.001", + "urn:epc:id:sgtin:0614141.107346.002", + "urn:epc:id:sgtin:0614141.107346.003" + ], + "action": "ADD", + "bizStep": "https://ref.gs1.org/cbv/BizStep-packing", + "bizLocation": {"id": "urn:epc:id:sgln:0614141.00001.0"} + }] + } +} +``` + +### Object Event with Sensor Data + +```json +{ + "@context": { + "@vocab": "https://gs1.github.io/EPCIS/", + "epcis": "https://gs1.github.io/EPCIS/", + "cbv": "https://ref.gs1.org/cbv/", + "type": "@type", + "id": "@id", + "epcisBody": "epcis:epcisBody", + "eventList": "epcis:eventList" + }, + "type": "EPCISDocument", + "schemaVersion": "2.0", + "creationDate": "2024-01-01T00:00:00Z", + "epcisBody": { + "eventList": [{ + "type": "ObjectEvent", + "eventTime": "2024-01-01T08:00:00.000Z", + "eventTimeZoneOffset": "+00:00", + "epcList": ["urn:epc:id:sgtin:0614141.107346.2017"], + "action": "OBSERVE", + "bizStep": "https://ref.gs1.org/cbv/BizStep-inspecting", + "disposition": "https://ref.gs1.org/cbv/Disp-conformant", + "readPoint": {"id": "urn:epc:id:sgln:0614141.00001.0"}, + "bizLocation": {"id": "urn:epc:id:sgln:0614141.00001.0"}, + "sensorElementList": [ + { + "sensorReport": [ + { + "type": "https://gs1.org/voc/MeasurementType-Temperature", + "time": "2024-01-01T08:00:00.000Z", + "value": 23.5, + "uom": "CEL" + } + ] + } + ] + }] + } +} +``` + +--- + +*Last updated: January 2026* +*For API details, see the interactive [Swagger documentation](/swagger)* + diff --git a/packages/plugin-epcis/eslint.config.mjs b/packages/plugin-epcis/eslint.config.mjs new file mode 100644 index 0000000..59c87c1 --- /dev/null +++ b/packages/plugin-epcis/eslint.config.mjs @@ -0,0 +1,4 @@ +import { config } from "@dkg/eslint-config/base"; + +/** @type {import("eslint").Linter.Config} */ +export default config; diff --git a/packages/plugin-epcis/package.json b/packages/plugin-epcis/package.json new file mode 100644 index 0000000..8b9abc8 --- /dev/null +++ b/packages/plugin-epcis/package.json @@ -0,0 +1,26 @@ +{ + "name": "@dkg/plugin-epcis", + "version": "0.0.1", + "description": "", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "scripts": { + "dev": "tsup src/*.ts --format cjs,esm --dts --watch", + "build": "tsup src/*.ts --format cjs,esm --dts", + "check-types": "tsc --noEmit", + "lint": "eslint . --max-warnings 0", + "test": "mocha --loader ../../node_modules/tsx/dist/loader.mjs 'tests/**/*.spec.ts'" + }, + "dependencies": { + "@dkg/plugin-swagger": "^0.0.2", + "@dkg/plugins": "^0.0.2", + "ajv": "^8.17.0", + "ajv-formats": "^3.0.0" + }, + "devDependencies": { + "@dkg/eslint-config": "*", + "@dkg/typescript-config": "*", + "tsup": "^8.5.0" + } +} diff --git a/packages/plugin-epcis/src/index.ts b/packages/plugin-epcis/src/index.ts new file mode 100644 index 0000000..8c41c20 --- /dev/null +++ b/packages/plugin-epcis/src/index.ts @@ -0,0 +1,436 @@ +import { defineDkgPlugin } from "@dkg/plugins"; +import { openAPIRoute, z } from "@dkg/plugin-swagger"; +import { EpcisValidationService } from "./services/EPCISValidationService"; +import { EpcisQueryService } from "./services/EPCISQueryService"; +import type { CaptureResponse } from "./model/types"; + +// Timeout for internal publisher requests (30s for POST, 5s for GET) +const PUBLISHER_POST_TIMEOUT_MS = 10000; +const PUBLISHER_GET_TIMEOUT_MS = 5000; + +// Helper function to send JSON-LD to publisher +async function sendToPublisher( + jsonLd: any, + metadata?: { source?: string; sourceId?: string }, + publishOptions?: { + privacy?: "private" | "public"; + epochs?: number; + } +): Promise<{ id: number; status: string; attemptCount: number }> { + const publisherUrl = process.env.PUBLISHER_URL || "http://localhost:9200"; + + try { + const response = await fetch(`${publisherUrl}/api/dkg/assets`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + content: jsonLd, + metadata: metadata || { source: "EPCIS" }, + publishOptions: { + privacy: publishOptions?.privacy ?? "private", + epochs: publishOptions?.epochs ?? 12, + }, + }), + signal: AbortSignal.timeout(PUBLISHER_POST_TIMEOUT_MS), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Publisher request failed"); + } + + return response.json(); + } catch (error: any) { + if (error.name === "TimeoutError") { + throw new Error("Publisher request timed out"); + } + throw error; + } +} + +export default defineDkgPlugin((ctx, mcp, api) => { + + const validationService = new EpcisValidationService(); + const queryService = new EpcisQueryService(); + + console.log("πŸš€ EPCIS Plugin loaded"); + + // MCP Tool: Query EPCIS events from DKG + mcp.registerTool( + "epcis-query", + { + title: "Query EPCIS Events", + description: + "Query EPCIS supply chain events from the OriginTrail DKG. " + + "Can filter by EPC (product identifier), from date to date, business step, or location. " + + "Use fullTrace=true to search across all event types (transformations, aggregations) for complete supply chain traceability.", + inputSchema: { + epc: z.string().optional().describe("EPC identifier (e.g., urn:epc:id:sgtin:0614141.107346.2017)"), + from: z.string().optional().describe("Query events from this date onwards, requires it to follow ISO 8601 format (e.g., 2024-01-01T00:00:00Z)"), + to: z.string().optional().describe("Query events up to this date, requires it to follow ISO 8601 format (e.g., 2024-01-01T00:00:00Z)"), + bizStep: z.string().optional().describe("Business step (e.g., 'receiving', 'shipping', 'assembling')"), + bizLocation: z.string().optional().describe("Business location URI"), + fullTrace: z.boolean().optional().describe("If true, search all EPC fields for full traceability"), + }, + }, + async (input) => { + try { + const sparqlQuery = queryService.buildQuery({ + epc: input.epc, + from: input.from, + to: input.to, + bizStep: input.bizStep, + bizLocation: input.bizLocation, + fullTrace: input.fullTrace, + }); + + const results = await ctx.dkg.graph.query(sparqlQuery, "SELECT"); + + const summary = results?.length + ? `Found ${results.length} EPCIS event(s)` + : "No events found matching the criteria"; + + return { + content: [ + { + type: "text", + text: JSON.stringify({ + summary, + count: results?.data.length || 0, + events: results || [], + //query: sparqlQuery, + }, null, 2) + } + ], + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: "Query failed", + message: error.message, + }, null, 2) + } + ], + isError: true, + }; + } + } + ); + + // MCP Tool: Track item journey (full traceability) + mcp.registerTool( + "epcis-track-item", + { + title: "Track Item Journey", + description: + "Track a single item's complete journey through the supply chain. " + + "Finds all events where this EPC appears - as observed item, transformation input/output, or in aggregations. " + + "Returns events in chronological order showing the item's full lifecycle.", + inputSchema: { + epc: z.string().describe("The EPC to track (e.g., urn:epc:id:sgtin:0614141.107346.2017)"), + }, + }, + async (input) => { + try { + const sparqlQuery = queryService.buildQuery({ + epc: input.epc, + fullTrace: true, // Always use full traceability for item tracking + }); + + const results = await ctx.dkg.graph.query(sparqlQuery, "SELECT"); + + const eventCount = results?.length || 0; + let summary = `Tracking: ${input.epc}\n`; + summary += `Found ${eventCount} event(s) in the supply chain.\n\n`; + + if (eventCount > 0) { + summary += "Journey Timeline:\n"; + results.forEach((event: any, idx: number) => { + const time = event.eventTime || "Unknown time"; + const step = event.bizStep?.split("-").pop() || event.eventType?.split("/").pop() || "Unknown"; + const location = event.bizLocation || event.readPoint || "Unknown location"; + summary += `${idx + 1}. [${time}] ${step} @ ${location}\n`; + }); + } + + return { + content: [ + { + type: "text", + text: JSON.stringify({ + summary, + epc: input.epc, + eventCount, + events: results || [], + }, null, 2) + } + ], + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: "Tracking failed", + message: error.message, + }, null, 2) + } + ], + isError: true, + }; + } + } + ); + + // POST /epcis/capture - Accept EPCISDocument and queue for publishing + api.post( + "/epcis/capture", + openAPIRoute( + { + tag: "EPCIS", + summary: "Capture EPCIS Document", + description: "Accept an EPCISDocument and queue it for publishing to DKG", + body: z.object({ + epcisDocument: z.object({}).passthrough().openapi({ + description: "The EPCISDocument (JSON-LD)", + }), + publishOptions: z.object({ + privacy: z.enum(["private", "public"]).optional().openapi({ + description: "Asset visibility (default: private)", + }), + epochs: z.number().min(1).optional().openapi({ + description: "Number of epochs to publish for (default: 12)", + }), + }).optional().openapi({ + description: "Publishing options (all optional with sensible defaults)", + }), + }), + response: { + description: "Capture accepted", + schema: z.object({ + status: z.string(), + receivedAt: z.string(), + captureID: z.string(), + eventCount: z.number(), + }), + }, + }, + async (req, res) => { + try { + const { epcisDocument, publishOptions } = req.body; + + // Validate the EPCIS document + const validation = validationService.validate(epcisDocument); + + if (!validation.valid) { + return res.status(400).json({ + error: "Invalid EPCISDocument", + details: validation.errors, + } as any); + } + + // Send to publisher with user-provided options (or defaults) + const result = await sendToPublisher( + epcisDocument, + { + source: "EPCIS", + sourceId: `epcis-${Date.now()}`, + }, + publishOptions + ); + + // Return capture response + const response: CaptureResponse = { + status: "202", + receivedAt: new Date().toISOString(), + captureID: String(result.id), + eventCount: validation.eventCount || 0, + }; + + res.status(202).json(response); + } catch (error: any) { + console.error("[EPCIS Capture] Error:", error); + res.status(500).json({ + error: "Failed to process capture", + //message: error.message, + } as any); + } + } + ) + ); + + // GET /epcis/capture/:captureID - Check capture status + api.get( + "/epcis/capture/:captureID", + openAPIRoute( + { + tag: "EPCIS", + summary: "Get Capture Status", + description: "Check the status of an EPCIS capture by captureID", + params: z.object({ + captureID: z.string().openapi({ + description: "The capture ID returned from POST /epcis/capture", + example: "123", + }), + }), + response: { + description: "Capture status", + schema: z.object({ + status: z.string(), + captureID: z.string(), + UAL: z.string().optional(), + publishedAt: z.string().optional(), + error: z.string().optional(), + }), + }, + }, + async (req, res) => { + try { + const { captureID } = req.params; + const publisherUrl = process.env.PUBLISHER_URL || "http://localhost:9200"; + + const captureIdPattern = /^[0-9]{1,20}$/; + if (!captureIdPattern.test(captureID)) { + return res.status(400).json({ + error: "Invalid captureID format", + captureID, + } as any); + } + // Query publisher for asset status + let response: Response; + try { + response = await fetch( + `${publisherUrl}/api/dkg/assets/status/${encodeURIComponent(captureID)}`, + { signal: AbortSignal.timeout(PUBLISHER_GET_TIMEOUT_MS) } + ); + } catch (error: any) { + if (error.name === "TimeoutError") { + return res.status(504).json({ + error: "Publisher timeout", + captureID, + } as any); + } + throw error; + } + + if (!response.ok) { + if (response.status === 404) { + return res.status(404).json({ error: "Capture not found", captureID } as any); + } + throw new Error("Failed to fetch capture status"); + } + + const asset = await response.json(); + + // Map publisher status to EPCIS response + const result: any = { + status: asset.status, + captureID, + }; + + if (asset.ual) result.UAL = asset.ual; + if (asset.publishedAt) result.publishedAt = asset.publishedAt; + if (asset.lastError) result.error = asset.lastError; + + res.json(result); + } catch (error: any) { + console.error("[EPCIS Status] Error:", error); + res.status(500).json({ + error: "Failed to get capture status", + //message: error.message, + } as any); + } + } + ) + ); + + // GET /epcis/events - Query EPCIS events from DKG + api.get( + "/epcis/events", + openAPIRoute( + { + tag: "EPCIS", + summary: "Query EPCIS Events", + description: "Query EPCIS events from DKG using various filters", + query: z.object({ + epc: z.string().optional().openapi({ + description: "Filter by EPC (product identifier)", + example: "urn:epc:id:sgtin:0614141.107346.2017", + }), + from: z.string().datetime({ message: "Must be ISO 8601 format (e.g., 2024-01-01T00:00:00Z)" }).optional().openapi({ + description: "Start of time range (ISO 8601)", + example: "2024-01-01T00:00:00Z", + }), + to: z.string().datetime({ message: "Must be ISO 8601 format (e.g., 2024-12-31T23:59:59Z)" }).optional().openapi({ + description: "End of time range (ISO 8601)", + example: "2024-12-31T23:59:59Z", + }), + bizStep: z.string().optional().openapi({ + description: "Filter by business step URI", + example: "https://ref.gs1.org/cbv/BizStep-assembling", + }), + bizLocation: z.string().optional().openapi({ + description: "Filter by business location", + example: "urn:epc:id:sgln:0614141.00001.0", + }), + /*ual: z.string().optional().openapi({ + description: "Get event by specific UAL", + }),*/ + fullTrace: z.string().optional().openapi({ + description: "If 'true', search all EPC fields (epcList, inputEPCList, outputEPCList, childEPCs, parentID) for full supply chain traceability", + example: "true", + }), + }), + response: { + description: "Query results", + schema: z.object({ + success: z.boolean(), + query: z.string().optional(), + results: z.array(z.any()), + count: z.number(), + }), + }, + }, + async (req, res) => { + try { + const { epc, from, to, bizStep, bizLocation, /*ual,*/ fullTrace } = req.query; + + // Build the SPARQL query based on parameters + const sparqlQuery = queryService.buildQuery({ + epc: epc as string, + from: from as string, + to: to as string, + bizStep: bizStep as string, + bizLocation: bizLocation as string, + //ual: ual as string, + fullTrace: fullTrace === 'true', + }); + + console.log("[EPCIS Events] Executing SPARQL query:", sparqlQuery); + + // Execute query against DKG + const results = await ctx.dkg.graph.query(sparqlQuery, "SELECT"); + + res.json({ + success: true, + //query: sparqlQuery, + results: results || [], + count: results?.length || 0, + }); + } catch (error: any) { + console.error("[EPCIS Events] Query error:", error); + res.status(500).json({ + success: false, + error: "Failed to query events", + message: error.message, + } as any); + } + } + ) + ); + +}); \ No newline at end of file diff --git a/packages/plugin-epcis/src/model/types.ts b/packages/plugin-epcis/src/model/types.ts new file mode 100644 index 0000000..ccf4ab8 --- /dev/null +++ b/packages/plugin-epcis/src/model/types.ts @@ -0,0 +1,50 @@ +// EPCIS Document types based on GS1 EPCIS 2.0 +export interface EPCISDocument { + "@context": string | string[] | Record; + type: "EPCISDocument"; + schemaVersion: string; + creationDate: string; + epcisBody?: { + eventList: EPCISEvent[]; + }; + eventList?: EPCISEvent[]; + [key: string]: any; + } + + export interface EPCISEvent { + type: string; + eventTime: string; + eventTimeZoneOffset?: string; + epcList?: string[]; + action?: string; + bizStep?: string; + disposition?: string; + readPoint?: { id: string }; + bizLocation?: { id: string }; + bizTransactionList?: Array<{ type: string; bizTransaction: string }>; + sensorElementList?: any[]; + [key: string]: any; + } + + // API Response types + export interface CaptureResponse { + status: string; + receivedAt: string; + captureID: string; + eventCount: number; + } + + export interface CaptureStatusResponse { + status: "pending" | "queued" | "assigned" | "publishing" | "published" | "failed"; + UAL?: string; + eventCount?: number; + error?: string; + publishedAt?: string | null; + } + + // Validation result type + export interface ValidationResult { + valid: boolean; + errors?: string[]; + eventCount?: number; + } \ No newline at end of file diff --git a/packages/plugin-epcis/src/schemas/epcis-json-schema.json b/packages/plugin-epcis/src/schemas/epcis-json-schema.json new file mode 100644 index 0000000..c1b5086 --- /dev/null +++ b/packages/plugin-epcis/src/schemas/epcis-json-schema.json @@ -0,0 +1,2352 @@ +{ + "$id": "https://ref.gs1.org/standards/epcis/2.0.0/epcis-json-schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + } + }, + "allOf": [ + { + "if": { + "type": "object", + "properties": { + "type": { + "enum": [ + "EPCISDocument" + ] + } + } + }, + "then": { + "$ref": "#/definitions/epcisDocument" + } + }, + { + "if": { + "type": "object", + "properties": { + "type": { + "enum": [ + "EPCISQueryDocument" + ] + } + } + }, + "then": { + "$ref": "#/definitions/epcisQueryDocument" + } + }, + { + "if": { + "type": "object", + "properties": { + "type": { + "enum": [ + "ObjectEvent" + ] + } + } + }, + "then": { + "allOf": [ + { + "$ref": "#/definitions/ObjectEvent" + }, + { + "$ref": "#/definitions/required-ld-context" + } + ] + } + }, + { + "if": { + "type": "object", + "properties": { + "type": { + "enum": [ + "AggregationEvent" + ] + } + } + }, + "then": { + "allOf": [ + { + "$ref": "#/definitions/AggregationEvent" + }, + { + "$ref": "#/definitions/required-ld-context" + } + ] + } + }, + { + "if": { + "type": "object", + "properties": { + "type": { + "enum": [ + "AssociationEvent" + ] + } + } + }, + "then": { + "allOf": [ + { + "$ref": "#/definitions/AssociationEvent" + }, + { + "$ref": "#/definitions/required-ld-context" + } + ] + } + }, + { + "if": { + "type": "object", + "properties": { + "type": { + "enum": [ + "TransformationEvent" + ] + } + } + }, + "then": { + "allOf": [ + { + "$ref": "#/definitions/TransformationEvent" + }, + { + "$ref": "#/definitions/required-ld-context" + } + ] + } + }, + { + "if": { + "type": "object", + "properties": { + "type": { + "enum": [ + "TransactionEvent" + ] + } + } + }, + "then": { + "allOf": [ + { + "$ref": "#/definitions/TransactionEvent" + }, + { + "$ref": "#/definitions/required-ld-context" + } + ] + } + }, + { + "if": { + "not": { + "type": "object", + "properties": { + "type": { + "enum": [ + "AssociationEvent", + "ObjectEvent", + "AggregationEvent", + "TransactionEvent", + "TransformationEvent", + "EPCISQueryDocument", + "EPCISDocument" + ] + } + } + } + }, + "then": { + "allOf": [ + { + "$ref": "#/definitions/Extended-Event" + }, + { + "$ref": "#/definitions/required-ld-context" + } + ] + } + } + ], + "definitions": { + "vocabulary": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/uri" + }, + "vocabularyElementList": { + "type": "array", + "items": { + "$ref": "#/definitions/vocabularyElement" + } + } + }, + "required": [ + "type" + ] + }, + "vocabularyElement": { + "type": "object", + "properties": { + "id": { + "$ref": "#/definitions/uri" + }, + "attributes": { + "type": "array", + "items": { + "$ref": "#/definitions/attribute" + } + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/uri" + } + } + }, + "required": [ + "id" + ] + }, + "attribute": { + "type": "object", + "properties": { + "id": { + "$ref": "#/definitions/uri" + }, + "attribute": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "object" + } + ] + } + }, + "required": [ + "id" + ] + }, + "eventList": { + "type": "array", + "items": { + "$ref": "#/definitions/EPCIS-Document-Event" + } + }, + "vocabularyList": { + "type": "array", + "items": { + "$ref": "#/definitions/vocabulary" + } + }, + "resultsBody": { + "type": "object", + "properties": { + "eventList": { + "$ref": "#/definitions/eventList" + }, + "vocabularyList": { + "$ref": "#/definitions/vocabularyList" + } + }, + "required": [ + "eventList" + ], + "propertyNames": { + "anyOf": [ + { + "type": "string", + "enum": [ + "eventList", + "vocabularyList" + ] + }, + { + "$ref": "#/definitions/vocab-uri" + } + ] + } + }, + "queryResults": { + "type": "object", + "properties": { + "queryName": { + "type": "string" + }, + "subscriptionID": { + "type": "string" + }, + "resultsBody": { + "$ref": "#/definitions/resultsBody" + } + }, + "required": [ + "queryName", + "resultsBody" + ], + "propertyNames": { + "anyOf": [ + { + "type": "string", + "enum": [ + "queryName", + "subscriptionID", + "resultsBody" + ] + }, + { + "$ref": "#/definitions/vocab-uri" + } + ] + } + }, + "epcisQueryDocumentBody": { + "type": "object", + "properties": { + "queryResults": { + "$ref": "#/definitions/queryResults" + } + }, + "required": [ + "queryResults" + ], + "propertyNames": { + "anyOf": [ + { + "type": "string", + "enum": [ + "queryResults" + ] + }, + { + "$ref": "#/definitions/vocab-uri" + } + ] + } + }, + "epcisHeader": { + "type": "object", + "properties": { + "epcisMasterData": { + "type": "object", + "properties": { + "vocabularyList": { + "type": "array", + "items": { + "$ref": "#/definitions/vocabulary" + } + } + } + } + }, + "propertyNames": { + "anyOf": [ + { + "type": "string", + "enum": [ + "epcisMasterData" + ] + }, + { + "$ref": "#/definitions/vocab-uri" + } + ] + } + }, + "epcisDocument": { + "type": "object", + "properties": { + "id": { + "$ref": "#/definitions/id" + }, + "type": { + "type": "string", + "enum": [ + "EPCISDocument" + ] + }, + "@context": { + "$ref": "#/definitions/@context" + }, + "schemaVersion": { + "$ref": "#/definitions/version" + }, + "creationDate": { + "$ref": "#/definitions/time" + }, + "instanceIdentifier": { + "type": "string" + }, + "sender": { + "type": "string" + }, + "receiver": { + "type": "string" + }, + "epcisHeader": { + "$ref": "#/definitions/epcisHeader" + }, + "epcisBody": { + "type": "object", + "properties": { + "eventList": { + "$ref": "#/definitions/eventList" + } + }, + "required": [ + "eventList" + ] + } + }, + "required": [ + "@context", + "type", + "schemaVersion", + "creationDate", + "epcisBody" + ], + "propertyNames": { + "anyOf": [ + { + "type": "string", + "enum": [ + "@context", + "id", + "type", + "schemaVersion", + "creationDate", + "instanceIdentifier", + "sender", + "receiver", + "epcisHeader", + "epcisBody" + ] + }, + { + "$ref": "#/definitions/vocab-uri" + } + ] + } + }, + "epcisQueryDocument": { + "type": "object", + "properties": { + "@context": { + "$ref": "#/definitions/@context" + }, + "id": { + "$ref": "#/definitions/id" + }, + "type": { + "type": "string", + "enum": [ + "EPCISQueryDocument" + ] + }, + "schemaVersion": { + "$ref": "#/definitions/version" + }, + "creationDate": { + "$ref": "#/definitions/time" + }, + "epcisBody": { + "$ref": "#/definitions/epcisQueryDocumentBody" + } + }, + "required": [ + "@context", + "type", + "epcisBody" + ], + "propertyNames": { + "anyOf": [ + { + "type": "string", + "enum": [ + "@context", + "id", + "type", + "schemaVersion", + "creationDate", + "epcisBody" + ] + }, + { + "$ref": "#/definitions/vocab-uri" + } + ] + } + }, + "EPCIS-Document": { + "oneOf": [ + { + "$ref": "#/definitions/epcisDocument" + }, + { + "$ref": "#/definitions/epcisQueryDocument" + } + ] + }, + "uri": { + "type": "string", + "format": "uri" + }, + "time": { + "type": "string", + "format": "date-time" + }, + "decimal": { + "type": "number" + }, + "boolean": { + "type": "boolean" + }, + "hexBinary": { + "type": "string", + "pattern": "^[A-Fa-f0-9]+$" + }, + "string": { + "type": "string" + }, + "action": { + "type": "string", + "enum": [ + "OBSERVE", + "ADD", + "DELETE" + ] + }, + "eventType": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ObjectEvent", + "AggregationEvent", + "AssociationEvent", + "TransformationEvent", + "TransactionEvent" + ] + }, + { + "type": "string", + "format": "uri" + } + ] + }, + "persistentDisposition": { + "allOf": [ + { + "type": "object", + "properties": { + "set": { + "type": "array", + "items": { + "$ref": "#/definitions/disposition" + }, + "minItems": 1, + "uniqueItems": true + }, + "unset": { + "type": "array", + "items": { + "$ref": "#/definitions/disposition" + }, + "minItems": 1, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + { + "anyOf": [ + { + "type": "object", + "required": [ + "set" + ] + }, + { + "type": "object", + "required": [ + "unset" + ] + } + ] + } + ] + }, + "epcList": { + "type": "array", + "items": { + "$ref": "#/definitions/uri" + }, + "uniqueItems": true + }, + "quantityList": { + "type": "array", + "items": { + "$ref": "#/definitions/quantityElement" + } + }, + "uom": { + "type": "string", + "pattern": "^[A-Z0-9]{2,3}$" + }, + "eventID": { + "$ref": "#/definitions/uri" + }, + "certificationInfo": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/definitions/uri" + } + }, + { + "$ref": "#/definitions/uri" + } + ] + }, + "errorDeclaration": { + "type": "object", + "properties": { + "declarationTime": { + "$ref": "#/definitions/time" + }, + "reason": { + "$ref": "#/definitions/error-reason" + }, + "correctiveEventIDs": { + "type": "array", + "items": { + "$ref": "#/definitions/eventID" + } + } + }, + "required": [ + "declarationTime" + ], + "propertyNames": { + "anyOf": [ + { + "$ref": "#/definitions/vocab-uri" + }, + { + "type": "string", + "enum": [ + "declarationTime", + "reason", + "correctiveEventIDs" + ] + } + ] + } + }, + "quantityElement": { + "type": "object", + "properties": { + "epcClass": { + "$ref": "#/definitions/uri" + }, + "quantity": { + "$ref": "#/definitions/decimal" + }, + "uom": { + "$ref": "#/definitions/uom" + } + }, + "required": [ + "epcClass" + ], + "additionalProperties": false + }, + "bizTransaction": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/bizTransaction-type" + }, + "bizTransaction": { + "$ref": "#/definitions/uri" + } + }, + "required": [ + "bizTransaction" + ], + "additionalProperties": false + }, + "readPoint": { + "type": "object", + "properties": { + "id": { + "$ref": "#/definitions/uri" + } + }, + "required": [ + "id" + ] + }, + "bizLocation": { + "type": "object", + "properties": { + "id": { + "$ref": "#/definitions/uri" + } + }, + "required": [ + "id" + ] + }, + "source": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/source-dest-type" + }, + "source": { + "$ref": "#/definitions/uri" + } + }, + "required": [ + "type", + "source" + ], + "additionalProperties": false + }, + "destination": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/source-dest-type" + }, + "destination": { + "$ref": "#/definitions/uri" + } + }, + "required": [ + "type", + "destination" + ], + "additionalProperties": false + }, + "sensorElement": { + "type": "object", + "properties": { + "sensorMetadata": { + "$ref": "#/definitions/sensorMetadata" + }, + "sensorReport": { + "$ref": "#/definitions/sensorReportList" + } + }, + "required": [ + "sensorReport" + ], + "propertyNames": { + "anyOf": [ + { + "$ref": "#/definitions/vocab-uri" + }, + { + "type": "string", + "enum": [ + "sensorMetadata", + "sensorReport" + ] + } + ] + } + }, + "sensorReportList": { + "type": "array", + "items": { + "$ref": "#/definitions/sensorReport" + }, + "minItems": 1 + }, + "sensorReport": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/measurementType" + }, + "exception": { + "$ref": "#/definitions/sensorAlertType" + }, + "deviceID": { + "$ref": "#/definitions/uri" + }, + "deviceMetadata": { + "$ref": "#/definitions/uri" + }, + "rawData": { + "$ref": "#/definitions/uri" + }, + "dataProcessingMethod": { + "$ref": "#/definitions/uri" + }, + "bizRules": { + "$ref": "#/definitions/uri" + }, + "time": { + "$ref": "#/definitions/time" + }, + "microorganism": { + "$ref": "#/definitions/uri" + }, + "chemicalSubstance": { + "$ref": "#/definitions/uri" + }, + "coordinateReferenceSystem": { + "$ref": "#/definitions/uri" + }, + "value": { + "$ref": "#/definitions/decimal" + }, + "component": { + "$ref": "#/definitions/component" + }, + "stringValue": { + "$ref": "#/definitions/string" + }, + "booleanValue": { + "$ref": "#/definitions/boolean" + }, + "hexBinaryValue": { + "$ref": "#/definitions/hexBinary" + }, + "uriValue": { + "$ref": "#/definitions/uri" + }, + "minValue": { + "$ref": "#/definitions/decimal" + }, + "maxValue": { + "$ref": "#/definitions/decimal" + }, + "meanValue": { + "$ref": "#/definitions/decimal" + }, + "sDev": { + "$ref": "#/definitions/decimal" + }, + "percRank": { + "$ref": "#/definitions/decimal" + }, + "percValue": { + "$ref": "#/definitions/decimal" + }, + "uom": { + "$ref": "#/definitions/string" + } + }, + "required": [ + "type" + ], + "propertyNames": { + "anyOf": [ + { + "$ref": "#/definitions/vocab-uri" + }, + { + "type": "string", + "enum": [ + "type", + "exception", + "deviceID", + "deviceMetadata", + "rawData", + "dataProcessingMethod", + "bizRules", + "time", + "microorganism", + "chemicalSubstance", + "coordinateReferenceSystem", + "value", + "component", + "stringValue", + "booleanValue", + "hexBinaryValue", + "uriValue", + "minValue", + "maxValue", + "meanValue", + "sDev", + "percRank", + "percValue", + "uom" + ] + } + ] + } + }, + "sensorMetadata": { + "type": "object", + "properties": { + "time": { + "$ref": "#/definitions/time" + }, + "deviceID": { + "$ref": "#/definitions/uri" + }, + "deviceMetadata": { + "$ref": "#/definitions/uri" + }, + "rawData": { + "$ref": "#/definitions/uri" + }, + "startTime": { + "$ref": "#/definitions/time" + }, + "endTime": { + "$ref": "#/definitions/time" + }, + "dataProcessingMethod": { + "$ref": "#/definitions/uri" + }, + "bizRules": { + "$ref": "#/definitions/uri" + } + }, + "propertyNames": { + "anyOf": [ + { + "$ref": "#/definitions/vocab-uri" + }, + { + "type": "string", + "enum": [ + "time", + "deviceID", + "deviceMetadata", + "rawData", + "startTime", + "endTime", + "dataProcessingMethod", + "bizRules" + ] + } + ] + } + }, + "ilmd": { + "type": "object", + "propertyNames": { + "type": "string", + "format": "uri" + } + }, + "Event": { + "type": "object", + "properties": { + "@context": { + "$ref": "#/definitions/@context" + }, + "eventTime": { + "$ref": "#/definitions/time" + }, + "recordTime": { + "$ref": "#/definitions/time" + }, + "eventTimeZoneOffset": { + "type": "string", + "pattern": "^([+]|[-])((0[0-9]|1[0-3]):([0-5][0-9])|14:00)$" + }, + "eventID": { + "$ref": "#/definitions/eventID" + }, + "certificationInfo": { + "$ref": "#/definitions/certificationInfo" + }, + "errorDeclaration": { + "$ref": "#/definitions/errorDeclaration" + } + }, + "required": [ + "eventTime", + "eventTimeZoneOffset" + ] + }, + "common-event-properties": { + "anyOf": [ + { + "type": "string", + "enum": [ + "@context", + "type", + "eventTime", + "recordTime", + "eventTimeZoneOffset", + "eventID", + "certificationInfo", + "errorDeclaration" + ] + }, + { + "$ref": "#/definitions/vocab-uri" + } + ] + }, + "Extended-Event": { + "allOf": [ + { + "$ref": "#/definitions/Event" + }, + { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/vocab-uri" + } + }, + "required": [ + "type" + ] + } + ] + }, + "disposition": { + "anyOf": [ + { + "$ref": "#/definitions/vocab-other-uri" + }, + { + "type": "string", + "enum": [ + "active", + "container_closed", + "damaged", + "destroyed", + "dispensed", + "disposed", + "encoded", + "expired", + "in_progress", + "in_transit", + "inactive", + "no_pedigree_match", + "non_sellable_other", + "partially_dispensed", + "recalled", + "reserved", + "retail_sold", + "returned", + "sellable_accessible", + "sellable_not_accessible", + "stolen", + "unknown", + "available", + "completeness_verified", + "completeness_inferred", + "conformant", + "container_open", + "mismatch_instance", + "mismatch_class", + "mismatch_quantity", + "needs_replacement", + "non_conformant", + "unavailable" + ] + } + ] + }, + "@context": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "object" + }, + { + "type": "array", + "uniqueItems": true, + "items": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "object" + } + ] + } + } + ] + }, + "vocab-uri": { + "type": "string", + "format": "uri" + }, + "vocab-other-uri": { + "type": "string", + "format": "uri", + "pattern": "^(?!(urn:epcglobal:cbv|https?:\\/\\/ns\\.gs1\\.org/cbv\\/))" + }, + "vocab-nonGS1WebVoc-uri": { + "type": "string", + "format": "uri", + "pattern": "^(?!(https?:\\/\\/gs1\\.org\\/voc\\/|https?:\\/\\/www\\.gs1\\.org\\/voc\\/))" + }, + "required-ld-context": { + "type": "object", + "required": [ + "@context" + ] + }, + "version": { + "type": "string", + "pattern": "^\\d+(\\.\\d+)*$" + }, + "id": { + "type": "string", + "format": "uri" + }, + "error-reason": { + "anyOf": [ + { + "$ref": "#/definitions/vocab-other-uri" + }, + { + "type": "string", + "enum": [ + "did_not_occur", + "incorrect_data" + ] + } + ] + }, + "bizTransaction-type": { + "anyOf": [ + { + "$ref": "#/definitions/vocab-other-uri" + }, + { + "type": "string", + "enum": [ + "bol", + "cert", + "desadv", + "inv", + "pedigree", + "po", + "poc", + "prodorder", + "recadv", + "rma", + "testprd", + "testres", + "upevt" + ] + } + ] + }, + "source-dest-type": { + "anyOf": [ + { + "$ref": "#/definitions/vocab-other-uri" + }, + { + "type": "string", + "enum": [ + "owning_party", + "possessing_party", + "location" + ] + } + ] + }, + "measurementType": { + "anyOf": [ + { + "$ref": "#/definitions/vocab-nonGS1WebVoc-uri" + }, + { + "type": "string", + "enum": [ + "AbsoluteHumidity", + "AbsorbedDose", + "AbsorbedDoseRate", + "Acceleration", + "Radioactivity", + "Altitude", + "AmountOfSubstance", + "AmountOfSubstancePerUnitVolume", + "Angle", + "AngularAcceleration", + "AngularMomentum", + "AngularVelocity", + "Area", + "Capacitance", + "Conductance", + "Conductivity", + "Count", + "Density", + "Dimensionless", + "DoseEquivalent", + "DoseEquivalentRate", + "DynamicViscosity", + "ElectricCharge", + "ElectricCurrent", + "ElectricCurrentDensity", + "ElectricFieldStrength", + "Energy", + "Exposure", + "Force", + "Frequency", + "Illuminance", + "Inductance", + "Irradiance", + "KinematicViscosity", + "Length", + "LinearMomentum", + "Luminance", + "LuminousFlux", + "LuminousIntensity", + "MagneticFlux", + "MagneticFluxDensity", + "MagneticVectorPotential", + "Mass", + "MassConcentration", + "MassFlowRate", + "MassPerAreaTime", + "MemoryCapacity", + "MolalityOfSolute", + "MolarEnergy", + "MolarMass", + "MolarVolume", + "Power", + "Pressure", + "RadiantFlux", + "RadiantIntensity", + "RelativeHumidity", + "Resistance", + "Resistivity", + "SolidAngle", + "SpecificVolume", + "Speed", + "SurfaceDensity", + "SurfaceTension", + "Temperature", + "Time", + "Torque", + "Voltage", + "Volume", + "VolumeFlowRate", + "VolumeFraction", + "VolumetricFlux", + "Wavenumber" + ] + } + ] + }, + "sensorAlertType": { + "anyOf": [ + { + "$ref": "#/definitions/vocab-nonGS1WebVoc-uri" + }, + { + "type": "string", + "enum": [ + "ALARM_CONDITION", + "ERROR_CONDITION" + ] + } + ] + }, + "component": { + "anyOf": [ + { + "$ref": "#/definitions/vocab-other-uri" + }, + { + "type": "string", + "enum": [ + "x", + "y", + "z", + "axial_distance", + "azimuth", + "height", + "spherical_radius", + "polar_angle", + "elevation_angle", + "easting", + "northing", + "latitude", + "longitude", + "altitude" + ] + } + ] + }, + "EPCIS-Document-Event": { + "type": "object", + "required": [ + "type" + ], + "allOf": [ + { + "if": { + "type": "object", + "properties": { + "type": { + "enum": [ + "ObjectEvent" + ] + } + } + }, + "then": { + "$ref": "#/definitions/ObjectEvent" + } + }, + { + "if": { + "type": "object", + "properties": { + "type": { + "enum": [ + "AggregationEvent" + ] + } + } + }, + "then": { + "$ref": "#/definitions/AggregationEvent" + } + }, + { + "if": { + "type": "object", + "properties": { + "type": { + "enum": [ + "TransactionEvent" + ] + } + } + }, + "then": { + "$ref": "#/definitions/TransactionEvent" + } + }, + { + "if": { + "properties": { + "type": { + "enum": [ + "TransformationEvent" + ] + } + } + }, + "then": { + "$ref": "#/definitions/TransformationEvent" + } + }, + { + "if": { + "type": "object", + "properties": { + "type": { + "enum": [ + "AssociationEvent" + ] + } + } + }, + "then": { + "$ref": "#/definitions/AssociationEvent" + } + }, + { + "if": { + "not": { + "type": "object", + "properties": { + "type": { + "enum": [ + "AssociationEvent", + "ObjectEvent", + "AggregationEvent", + "TransactionEvent", + "TransformationEvent" + ] + } + } + } + }, + "then": { + "$ref": "#/definitions/Extended-Event" + } + } + ], + "properties": { + "type": { + "type": "string" + } + } + }, + "ObjectEvent": { + "allOf": [ + { + "$ref": "#/definitions/Event" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ObjectEvent" + ] + }, + "epcList": { + "$ref": "#/definitions/epcList" + }, + "quantityList": { + "$ref": "#/definitions/quantityList" + }, + "action": { + "$ref": "#/definitions/action" + }, + "bizStep": { + "$ref": "#/definitions/bizStep" + }, + "disposition": { + "$ref": "#/definitions/disposition" + }, + "persistentDisposition": { + "$ref": "#/definitions/persistentDisposition" + }, + "readPoint": { + "$ref": "#/definitions/readPoint" + }, + "bizLocation": { + "$ref": "#/definitions/bizLocation" + }, + "bizTransactionList": { + "type": "array", + "items": { + "$ref": "#/definitions/bizTransaction" + } + }, + "sourceList": { + "type": "array", + "items": { + "$ref": "#/definitions/source" + } + }, + "destinationList": { + "type": "array", + "items": { + "$ref": "#/definitions/destination" + } + }, + "sensorElementList": { + "type": "array", + "items": { + "$ref": "#/definitions/sensorElement" + } + }, + "ilmd": { + "$ref": "#/definitions/ilmd" + } + }, + "required": [ + "type", + "action" + ], + "propertyNames": { + "anyOf": [ + { + "$ref": "#/definitions/common-event-properties" + }, + { + "type": "string", + "enum": [ + "action", + "epcList", + "quantityList", + "bizStep", + "disposition", + "persistentDisposition", + "readPoint", + "bizLocation", + "bizTransactionList", + "sourceList", + "destinationList", + "sensorElementList", + "ilmd" + ] + } + ] + } + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "epcList": { + "type": "array", + "minItems": 0, + "items": { + "$ref": "#/definitions/id" + } + } + }, + "required": [ + "epcList" + ] + }, + { + "type": "object", + "properties": { + "quantityList": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/quantityElement" + } + } + }, + "required": [ + "quantityList" + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "sensorElementList": { + "type": "array", + "items": { + "$ref": "#/definitions/sensorElement" + }, + "minItems": 1 + } + }, + "required": [ + "sensorElementList" + ] + }, + { + "type": "object", + "properties": { + "readPoint": { + "$ref": "#/definitions/readPoint" + } + }, + "required": [ + "readPoint" + ] + } + ] + } + ] + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "ilmd": { + "not": {} + }, + "action": { + "type": "string", + "pattern": "^OBSERVE$" + } + } + }, + { + "type": "object", + "properties": { + "ilmd": { + "not": {} + }, + "action": { + "type": "string", + "pattern": "^DELETE$" + } + } + }, + { + "type": "object", + "properties": { + "action": { + "type": "string", + "pattern": "^ADD$" + } + } + } + ] + } + ] + }, + "bizStep": { + "anyOf": [ + { + "$ref": "#/definitions/vocab-other-uri" + }, + { + "type": "string", + "enum": [ + "accepting", + "arriving", + "assembling", + "collecting", + "commissioning", + "consigning", + "creating_class_instance", + "cycle_counting", + "decommissioning", + "departing", + "destroying", + "disassembling", + "dispensing", + "encoding", + "entering_exiting", + "holding", + "inspecting", + "installing", + "killing", + "loading", + "other", + "packing", + "picking", + "receiving", + "removing", + "repackaging", + "repairing", + "replacing", + "reserving", + "retail_selling", + "shipping", + "staging_outbound", + "stock_taking", + "stocking", + "storing", + "transporting", + "unloading", + "unpacking", + "void_shipping", + "sensor_reporting", + "sampling" + ] + } + ] + }, + "AggregationEvent": { + "allOf": [ + { + "$ref": "#/definitions/Event" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "AggregationEvent" + ] + }, + "parentID": { + "$ref": "#/definitions/uri" + }, + "childEPCs": { + "type": "array", + "items": { + "$ref": "#/definitions/uri" + } + }, + "childQuantityList": { + "type": "array", + "items": { + "$ref": "#/definitions/quantityElement" + } + }, + "action": { + "$ref": "#/definitions/action" + }, + "bizStep": { + "$ref": "#/definitions/bizStep" + }, + "disposition": { + "$ref": "#/definitions/disposition" + }, + "readPoint": { + "$ref": "#/definitions/readPoint" + }, + "bizLocation": { + "$ref": "#/definitions/bizLocation" + }, + "bizTransactionList": { + "type": "array", + "items": { + "$ref": "#/definitions/bizTransaction" + } + }, + "sourceList": { + "type": "array", + "items": { + "$ref": "#/definitions/source" + } + }, + "destinationList": { + "type": "array", + "items": { + "$ref": "#/definitions/destination" + } + }, + "sensorElementList": { + "type": "array", + "items": { + "$ref": "#/definitions/sensorElement" + } + } + }, + "required": [ + "type", + "action" + ], + "propertyNames": { + "anyOf": [ + { + "$ref": "#/definitions/common-event-properties" + }, + { + "type": "string", + "enum": [ + "parentID", + "childEPCs", + "childQuantityList", + "action", + "bizStep", + "disposition", + "persistentDisposition", + "readPoint", + "bizLocation", + "bizTransactionList", + "sourceList", + "destinationList", + "sensorElementList" + ] + } + ] + } + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "childEPCs": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/id" + } + } + }, + "required": [ + "childEPCs" + ] + }, + { + "type": "object", + "properties": { + "childQuantityList": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/quantityElement" + } + } + }, + "required": [ + "childQuantityList" + ] + }, + { + "type": "object", + "properties": { + "action": { + "type": "string", + "pattern": "^DELETE$" + } + } + } + ] + } + ] + }, + "TransactionEvent": { + "allOf": [ + { + "$ref": "#/definitions/Event" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "TransactionEvent" + ] + }, + "bizTransactionList": { + "type": "array", + "items": { + "$ref": "#/definitions/bizTransaction" + }, + "minItems": 1 + }, + "parentID": { + "$ref": "#/definitions/uri" + }, + "epcList": { + "type": "array", + "items": { + "$ref": "#/definitions/uri" + } + }, + "quantityList": { + "type": "array", + "items": { + "$ref": "#/definitions/quantityElement" + } + }, + "action": { + "$ref": "#/definitions/action" + }, + "bizStep": { + "$ref": "#/definitions/bizStep" + }, + "disposition": { + "$ref": "#/definitions/disposition" + }, + "readPoint": { + "$ref": "#/definitions/readPoint" + }, + "bizLocation": { + "$ref": "#/definitions/bizLocation" + }, + "sourceList": { + "type": "array", + "items": { + "$ref": "#/definitions/source" + } + }, + "destinationList": { + "type": "array", + "items": { + "$ref": "#/definitions/destination" + } + }, + "sensorElementList": { + "type": "array", + "items": { + "$ref": "#/definitions/sensorElement" + } + } + }, + "required": [ + "type", + "bizTransactionList", + "action" + ], + "propertyNames": { + "anyOf": [ + { + "$ref": "#/definitions/common-event-properties" + }, + { + "type": "string", + "enum": [ + "bizTransactionList", + "parentID", + "epcList", + "quantityList", + "action", + "bizStep", + "disposition", + "persistentDisposition", + "readPoint", + "bizLocation", + "sourceList", + "destinationList", + "sensorElementList" + ] + } + ] + } + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "epcList": { + "type": "array", + "minItems": 0, + "items": { + "$ref": "#/definitions/id" + } + } + }, + "required": [ + "epcList" + ] + }, + { + "type": "object", + "properties": { + "quantityList": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/quantityElement" + } + } + }, + "required": [ + "quantityList" + ] + }, + { + "type": "object", + "properties": { + "action": { + "type": "string", + "pattern": "^DELETE$" + } + } + } + ] + } + ] + }, + "TransformationEvent": { + "allOf": [ + { + "$ref": "#/definitions/Event" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "TransformationEvent" + ] + }, + "inputEPCList": { + "$ref": "#/definitions/epcList" + }, + "inputQuantityList": { + "$ref": "#/definitions/quantityList" + }, + "outputEPCList": { + "$ref": "#/definitions/epcList" + }, + "outputQuantityList": { + "$ref": "#/definitions/quantityList" + }, + "transformationID": { + "$ref": "#/definitions/uri" + }, + "bizStep": { + "$ref": "#/definitions/bizStep" + }, + "disposition": { + "$ref": "#/definitions/disposition" + }, + "persistentDisposition": { + "$ref": "#/definitions/persistentDisposition" + }, + "readPoint": { + "$ref": "#/definitions/readPoint" + }, + "bizLocation": { + "$ref": "#/definitions/bizLocation" + }, + "bizTransactionList": { + "type": "array", + "items": { + "$ref": "#/definitions/bizTransaction" + } + }, + "sourceList": { + "type": "array", + "items": { + "$ref": "#/definitions/source" + } + }, + "destinationList": { + "type": "array", + "items": { + "$ref": "#/definitions/destination" + } + }, + "sensorElementList": { + "type": "array", + "items": { + "$ref": "#/definitions/sensorElement" + } + }, + "ilmd": { + "$ref": "#/definitions/ilmd" + } + }, + "required": [ + "type" + ], + "propertyNames": { + "anyOf": [ + { + "$ref": "#/definitions/common-event-properties" + }, + { + "type": "string", + "enum": [ + "inputEPCList", + "inputQuantityList", + "outputEPCList", + "outputQuantityList", + "transformationID", + "bizStep", + "disposition", + "persistentDisposition", + "readPoint", + "bizLocation", + "bizTransactionList", + "sourceList", + "destinationList", + "sensorElementList", + "ilmd" + ] + } + ] + } + }, + { + "anyOf": [ + { + "allOf": [ + { + "anyOf": [ + { + "type": "object", + "properties": { + "inputEPCList": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + }, + "required": [ + "inputEPCList" + ] + }, + { + "type": "object", + "properties": { + "inputQuantityList": { + "type": "array", + "minItems": 1, + "items": { + "type": "object" + } + } + }, + "required": [ + "inputQuantityList" + ] + } + ] + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "outputEPCList": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + }, + "required": [ + "outputEPCList" + ] + }, + { + "type": "object", + "properties": { + "outputQuantityList": { + "type": "array", + "minItems": 1, + "items": { + "type": "object" + } + } + }, + "required": [ + "outputQuantityList" + ] + } + ] + } + ] + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "inputEPCList": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + }, + "required": [ + "inputEPCList" + ] + }, + { + "type": "object", + "properties": { + "inputQuantityList": { + "type": "array", + "minItems": 1, + "items": { + "type": "object" + } + } + }, + "required": [ + "inputQuantityList" + ] + }, + { + "type": "object", + "properties": { + "outputEPCList": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + }, + "required": [ + "outputEPCList" + ] + }, + { + "type": "object", + "properties": { + "outputQuantityList": { + "type": "array", + "minItems": 1, + "items": { + "type": "object" + } + } + }, + "required": [ + "outputQuantityList" + ] + } + ], + "type": "object", + "required": [ + "transformationID" + ] + } + ] + } + ] + }, + "AssociationEvent": { + "allOf": [ + { + "$ref": "#/definitions/Event" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "AssociationEvent" + ] + }, + "parentID": { + "$ref": "#/definitions/uri" + }, + "childEPCs": { + "type": "array", + "items": { + "$ref": "#/definitions/uri" + } + }, + "childQuantityList": { + "type": "array", + "items": { + "$ref": "#/definitions/quantityElement" + } + }, + "action": { + "$ref": "#/definitions/action" + }, + "bizStep": { + "$ref": "#/definitions/bizStep" + }, + "disposition": { + "$ref": "#/definitions/disposition" + }, + "readPoint": { + "$ref": "#/definitions/readPoint" + }, + "bizLocation": { + "$ref": "#/definitions/bizLocation" + }, + "bizTransactionList": { + "type": "array", + "items": { + "$ref": "#/definitions/bizTransaction" + } + }, + "sourceList": { + "type": "array", + "items": { + "$ref": "#/definitions/source" + } + }, + "destinationList": { + "type": "array", + "items": { + "$ref": "#/definitions/destination" + } + }, + "sensorElementList": { + "type": "array", + "items": { + "$ref": "#/definitions/sensorElement" + } + } + }, + "required": [ + "type", + "action", + "parentID" + ], + "propertyNames": { + "anyOf": [ + { + "$ref": "#/definitions/common-event-properties" + }, + { + "type": "string", + "enum": [ + "parentID", + "childEPCs", + "childQuantityList", + "action", + "bizStep", + "disposition", + "persistentDisposition", + "readPoint", + "bizLocation", + "bizTransactionList", + "sourceList", + "destinationList", + "sensorElementList" + ] + } + ] + } + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "childEPCs": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/id" + } + } + }, + "required": [ + "childEPCs" + ] + }, + { + "type": "object", + "properties": { + "childQuantityList": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/quantityElement" + } + } + }, + "required": [ + "childQuantityList" + ] + }, + { + "type": "object", + "properties": { + "action": { + "type": "string", + "pattern": "^DELETE$" + } + } + } + ] + } + ] + } + } +} diff --git a/packages/plugin-epcis/src/services/EPCISQueryService.ts b/packages/plugin-epcis/src/services/EPCISQueryService.ts new file mode 100644 index 0000000..a867014 --- /dev/null +++ b/packages/plugin-epcis/src/services/EPCISQueryService.ts @@ -0,0 +1,154 @@ +/** + * EPCIS Query Service + * Supports composite filtering - combine multiple filters in one query + */ + +// Namespace prefixes for EPCIS queries +const PREFIXES = ` +PREFIX epcis: +PREFIX schema: +PREFIX xsd: +`; + +export interface EpcisQueryParams { + epc?: string; + from?: string; + to?: string; + bizStep?: string; + bizLocation?: string; + // ual?: string; // TODO: Re-enable when UAL query is implemented + /** If true, searches all EPC fields (epcList, inputEPCList, outputEPCList, childEPCs, parentID) */ + fullTrace?: boolean; +} + +/** + * Escape special characters in SPARQL string literals + */ +function escapeSparql(value: string): string { + return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +/** + * Normalize bizStep to full GS1 CBV URI + * Accepts: "assembling" or "https://ref.gs1.org/cbv/BizStep-assembling" + */ +function normalizeBizStep(value: string): string { + + if (typeof value !== "string" || value.length === 0) { + throw new Error("Invalid bizStep value"); + } + + if (!value.includes('://')) { + return `https://ref.gs1.org/cbv/BizStep-${value}`; + } + return value; +} + +export class EpcisQueryService { + /** + * Build a composite SPARQL query supporting multiple filters + * All provided filters are combined with AND logic + */ + buildQuery(params: EpcisQueryParams): string { + // Special case: UAL lookup returns all triples for that graph + /*if (params.ual) { + return this.getEventByUal(params.ual); + }*/ + + const wherePatterns: string[] = []; + const filterClauses: string[] = []; + const optionalClauses: string[] = []; + + // Base pattern - always present + wherePatterns.push('?event a ?eventType .'); + + // Filter by event type (must be EPCIS event) + filterClauses.push('FILTER(STRSTARTS(STR(?eventType), "https://gs1.github.io/EPCIS/"))'); + + // EPC filter - with optional full traceability across all EPC fields + if (params.epc) { + const epcValue = escapeSparql(params.epc); + if (params.fullTrace) { + // Search across ALL EPC fields for full supply chain traceability + wherePatterns.push(`{ + { ?event epcis:epcList "${epcValue}" } + UNION { ?event epcis:inputEPCList "${epcValue}" } + UNION { ?event epcis:outputEPCList "${epcValue}" } + UNION { ?event epcis:childEPCs "${epcValue}" } + UNION { ?event epcis:parentID "${epcValue}" } + }`); + } else { + // Default: only search epcList + wherePatterns.push(`?event epcis:epcList "${epcValue}" .`); + } + } else { + optionalClauses.push('OPTIONAL { ?event epcis:epcList ?epc . }'); + } + + // BizStep filter (accepts shorthand like "assembling" or full URI) + if (params.bizStep) { + const bizStepUri = normalizeBizStep(params.bizStep); + wherePatterns.push('?event epcis:bizStep ?bizStep .'); + filterClauses.push(`FILTER(STR(?bizStep) = "${escapeSparql(bizStepUri)}")`); + } else { + optionalClauses.push('OPTIONAL { ?event epcis:bizStep ?bizStep . }'); + } + + // BizLocation filter + if (params.bizLocation) { + wherePatterns.push(`?event epcis:bizLocation "${escapeSparql(params.bizLocation)}" .`); + } else { + optionalClauses.push('OPTIONAL { ?event epcis:bizLocation ?bizLocation . }'); + } + + // Time range filter - use xsd:dateTime for proper date comparison + if (params.from || params.to) { + wherePatterns.push('?event epcis:eventTime ?eventTime .'); + if (params.from && params.to) { + filterClauses.push( + `FILTER(xsd:dateTime(?eventTime) >= xsd:dateTime("${escapeSparql(params.from)}") && xsd:dateTime(?eventTime) <= xsd:dateTime("${escapeSparql(params.to)}"))` + ); + } else if (params.from) { + filterClauses.push(`FILTER(xsd:dateTime(?eventTime) >= xsd:dateTime("${escapeSparql(params.from)}"))`); + } else if (params.to) { + filterClauses.push(`FILTER(xsd:dateTime(?eventTime) <= xsd:dateTime("${escapeSparql(params.to)}"))`); + } + } else { + optionalClauses.push('OPTIONAL { ?event epcis:eventTime ?eventTime . }'); + } + + // Always optional fields + optionalClauses.push('OPTIONAL { ?event epcis:disposition ?disposition . }'); + optionalClauses.push('OPTIONAL { ?event epcis:readPoint ?readPoint . }'); + + // Assemble the query + return `${PREFIXES} +SELECT ?ual ?eventType ?eventTime ?epc ?bizStep ?disposition ?readPoint ?bizLocation +WHERE { + GRAPH ?ual { + ${wherePatterns.join('\n ')} + ${optionalClauses.join('\n ')} + } + ${filterClauses.join('\n ')} +} +ORDER BY DESC(?eventTime) +LIMIT 100`; + } + + /** + * Query event by UAL (get full event details) + */ + /*private getEventByUal(ual: string): string { + // Basic UAL format validation + if (!ual.startsWith('did:')) { + throw new Error('Invalid UAL format'); + } + return `${PREFIXES} +SELECT ?predicate ?object +WHERE { + GRAPH <${escapeSparql(ual)}> { + ?subject ?predicate ?object . + } +}`; + }*/ +} diff --git a/packages/plugin-epcis/src/services/EPCISValidationService.ts b/packages/plugin-epcis/src/services/EPCISValidationService.ts new file mode 100644 index 0000000..db77cdd --- /dev/null +++ b/packages/plugin-epcis/src/services/EPCISValidationService.ts @@ -0,0 +1,55 @@ +import Ajv from "ajv"; +import addFormats from "ajv-formats"; +import epcisSchema from "../schemas/epcis-json-schema.json"; +import type { EPCISDocument, ValidationResult } from "../model/types"; + +export class EpcisValidationService { + private ajv: Ajv; + private validateSchema: ReturnType; + + constructor() { + this.ajv = new Ajv({ + allErrors: true, + strict: false, + validateFormats: true, + }); + addFormats(this.ajv); + + // Compile the EPCIS schema + this.validateSchema = this.ajv.compile(epcisSchema); + } + + /** + * Validate an EPCISDocument against the GS1 JSON Schema + */ + validate(document: unknown): ValidationResult { + const isValid = this.validateSchema(document); + + if (!isValid) { + const errors = this.validateSchema.errors?.map((err) => { + return `${err.instancePath || "/"}: ${err.message}`; + }) || ["Unknown validation error"]; + + return { + valid: false, + errors, + }; + } + + // Count events for response + const doc = document as EPCISDocument; + const eventList = doc.epcisBody?.eventList || []; + + return { + valid: true, + eventCount: eventList.length, + }; + } + + /** + * Extract events from an EPCISDocument + */ + extractEvents(document: EPCISDocument): EPCISDocument["eventList"] { + return document.eventList || document.epcisBody?.eventList || []; + } +} \ No newline at end of file diff --git a/packages/plugin-epcis/test-data/bicycle-manufacturing-story.json b/packages/plugin-epcis/test-data/bicycle-manufacturing-story.json new file mode 100644 index 0000000..dcfe3ca --- /dev/null +++ b/packages/plugin-epcis/test-data/bicycle-manufacturing-story.json @@ -0,0 +1,399 @@ +{ + "story": "Alpine Cycles Bicycle Manufacturing - Full Traceability Demo", + "description": "This dataset tells the story of manufacturing premium bicycles, from receiving raw components to shipping finished products. Use these events to test EPCIS querying and item tracking.", + + "characters": { + "components": { + "frame": "urn:epc:id:sgtin:4012345.011111.1001", + "frontWheel": "urn:epc:id:sgtin:4012345.022222.2001", + "rearWheel": "urn:epc:id:sgtin:4012345.022222.2002", + "handlebar": "urn:epc:id:sgtin:4012345.033333.3001" + }, + "finishedProduct": { + "bicycle": "urn:epc:id:sgtin:4012345.099999.9001" + }, + "container": { + "pallet": "urn:epc:id:sscc:4012345.0000000001" + } + }, + + "locations": { + "receivingDock": "urn:epc:id:sgln:4012345.00001.0", + "qualityLab": "urn:epc:id:sgln:4012345.00002.0", + "assemblyLine": "urn:epc:id:sgln:4012345.00003.0", + "packingArea": "urn:epc:id:sgln:4012345.00004.0", + "shippingDock": "urn:epc:id:sgln:4012345.00005.0" + }, + + "events": [ + { + "name": "Event 1: Receive Frame", + "description": "Carbon fiber frame arrives from supplier", + "request": { + "epcisDocument": { + "@context": { + "@vocab": "https://gs1.github.io/EPCIS/", + "epcis": "https://gs1.github.io/EPCIS/", + "cbv": "https://ref.gs1.org/cbv/", + "type": "@type", + "id": "@id" + }, + "type": "EPCISDocument", + "schemaVersion": "2.0", + "creationDate": "2024-03-01T08:00:00Z", + "epcisBody": { + "eventList": [{ + "type": "ObjectEvent", + "eventTime": "2024-03-01T08:00:00.000Z", + "eventTimeZoneOffset": "+00:00", + "epcList": ["urn:epc:id:sgtin:4012345.011111.1001"], + "action": "ADD", + "bizStep": "https://ref.gs1.org/cbv/BizStep-receiving", + "disposition": "https://ref.gs1.org/cbv/Disp-in_progress", + "readPoint": {"id": "urn:epc:id:sgln:4012345.00001.0"}, + "bizLocation": {"id": "urn:epc:id:sgln:4012345.00001.0"}, + "bizTransactionList": [ + {"type": "https://ref.gs1.org/cbv/BTT-po", "bizTransaction": "urn:epc:id:gdti:4012345.00001.PO-2024-001"} + ] + }] + } + }, + "publishOptions": { + "privacy": "private", + "epochs": 12 + } + } + }, + { + "name": "Event 2: Receive Wheels", + "description": "Front and rear wheels arrive from wheel supplier", + "request": { + "epcisDocument": { + "@context": { + "@vocab": "https://gs1.github.io/EPCIS/", + "epcis": "https://gs1.github.io/EPCIS/", + "cbv": "https://ref.gs1.org/cbv/", + "type": "@type", + "id": "@id" + }, + "type": "EPCISDocument", + "schemaVersion": "2.0", + "creationDate": "2024-03-01T08:30:00Z", + "epcisBody": { + "eventList": [{ + "type": "ObjectEvent", + "eventTime": "2024-03-01T08:30:00.000Z", + "eventTimeZoneOffset": "+00:00", + "epcList": [ + "urn:epc:id:sgtin:4012345.022222.2001", + "urn:epc:id:sgtin:4012345.022222.2002" + ], + "action": "ADD", + "bizStep": "https://ref.gs1.org/cbv/BizStep-receiving", + "disposition": "https://ref.gs1.org/cbv/Disp-in_progress", + "readPoint": {"id": "urn:epc:id:sgln:4012345.00001.0"}, + "bizLocation": {"id": "urn:epc:id:sgln:4012345.00001.0"}, + "bizTransactionList": [ + {"type": "https://ref.gs1.org/cbv/BTT-po", "bizTransaction": "urn:epc:id:gdti:4012345.00001.PO-2024-002"} + ] + }] + } + }, + "publishOptions": { + "privacy": "private", + "epochs": 12 + } + } + }, + { + "name": "Event 3: Receive Handlebar", + "description": "Aluminum handlebar arrives", + "request": { + "epcisDocument": { + "@context": { + "@vocab": "https://gs1.github.io/EPCIS/", + "epcis": "https://gs1.github.io/EPCIS/", + "cbv": "https://ref.gs1.org/cbv/", + "type": "@type", + "id": "@id" + }, + "type": "EPCISDocument", + "schemaVersion": "2.0", + "creationDate": "2024-03-01T09:00:00Z", + "epcisBody": { + "eventList": [{ + "type": "ObjectEvent", + "eventTime": "2024-03-01T09:00:00.000Z", + "eventTimeZoneOffset": "+00:00", + "epcList": ["urn:epc:id:sgtin:4012345.033333.3001"], + "action": "ADD", + "bizStep": "https://ref.gs1.org/cbv/BizStep-receiving", + "disposition": "https://ref.gs1.org/cbv/Disp-in_progress", + "readPoint": {"id": "urn:epc:id:sgln:4012345.00001.0"}, + "bizLocation": {"id": "urn:epc:id:sgln:4012345.00001.0"} + }] + } + }, + "publishOptions": { + "privacy": "private", + "epochs": 12 + } + } + }, + { + "name": "Event 4: Inspect Frame", + "description": "Quality check on carbon fiber frame - PASSED", + "request": { + "epcisDocument": { + "@context": { + "@vocab": "https://gs1.github.io/EPCIS/", + "epcis": "https://gs1.github.io/EPCIS/", + "cbv": "https://ref.gs1.org/cbv/", + "type": "@type", + "id": "@id" + }, + "type": "EPCISDocument", + "schemaVersion": "2.0", + "creationDate": "2024-03-01T10:00:00Z", + "epcisBody": { + "eventList": [{ + "type": "ObjectEvent", + "eventTime": "2024-03-01T10:00:00.000Z", + "eventTimeZoneOffset": "+00:00", + "epcList": ["urn:epc:id:sgtin:4012345.011111.1001"], + "action": "OBSERVE", + "bizStep": "https://ref.gs1.org/cbv/BizStep-inspecting", + "disposition": "https://ref.gs1.org/cbv/Disp-conformant", + "readPoint": {"id": "urn:epc:id:sgln:4012345.00002.0"}, + "bizLocation": {"id": "urn:epc:id:sgln:4012345.00002.0"} + }] + } + }, + "publishOptions": { + "privacy": "private", + "epochs": 12 + } + } + }, + { + "name": "Event 5: Inspect Wheels", + "description": "Quality check on both wheels - PASSED", + "request": { + "epcisDocument": { + "@context": { + "@vocab": "https://gs1.github.io/EPCIS/", + "epcis": "https://gs1.github.io/EPCIS/", + "cbv": "https://ref.gs1.org/cbv/", + "type": "@type", + "id": "@id" + }, + "type": "EPCISDocument", + "schemaVersion": "2.0", + "creationDate": "2024-03-01T10:30:00Z", + "epcisBody": { + "eventList": [{ + "type": "ObjectEvent", + "eventTime": "2024-03-01T10:30:00.000Z", + "eventTimeZoneOffset": "+00:00", + "epcList": [ + "urn:epc:id:sgtin:4012345.022222.2001", + "urn:epc:id:sgtin:4012345.022222.2002" + ], + "action": "OBSERVE", + "bizStep": "https://ref.gs1.org/cbv/BizStep-inspecting", + "disposition": "https://ref.gs1.org/cbv/Disp-conformant", + "readPoint": {"id": "urn:epc:id:sgln:4012345.00002.0"}, + "bizLocation": {"id": "urn:epc:id:sgln:4012345.00002.0"} + }] + } + }, + "publishOptions": { + "privacy": "private", + "epochs": 12 + } + } + }, + { + "name": "Event 6: Assemble Bicycle", + "description": "πŸ”§ TRANSFORMATION: Components assembled into finished bicycle", + "request": { + "epcisDocument": { + "@context": { + "@vocab": "https://gs1.github.io/EPCIS/", + "epcis": "https://gs1.github.io/EPCIS/", + "cbv": "https://ref.gs1.org/cbv/", + "type": "@type", + "id": "@id" + }, + "type": "EPCISDocument", + "schemaVersion": "2.0", + "creationDate": "2024-03-01T14:00:00Z", + "epcisBody": { + "eventList": [{ + "type": "TransformationEvent", + "eventTime": "2024-03-01T14:00:00.000Z", + "eventTimeZoneOffset": "+00:00", + "inputEPCList": [ + "urn:epc:id:sgtin:4012345.011111.1001", + "urn:epc:id:sgtin:4012345.022222.2001", + "urn:epc:id:sgtin:4012345.022222.2002", + "urn:epc:id:sgtin:4012345.033333.3001" + ], + "outputEPCList": [ + "urn:epc:id:sgtin:4012345.099999.9001" + ], + "bizStep": "https://ref.gs1.org/cbv/BizStep-assembling", + "disposition": "https://ref.gs1.org/cbv/Disp-active", + "readPoint": {"id": "urn:epc:id:sgln:4012345.00003.0"}, + "bizLocation": {"id": "urn:epc:id:sgln:4012345.00003.0"} + }] + } + }, + "publishOptions": { + "privacy": "private", + "epochs": 12 + } + } + }, + { + "name": "Event 7: Final Quality Check", + "description": "Finished bicycle passes final inspection", + "request": { + "epcisDocument": { + "@context": { + "@vocab": "https://gs1.github.io/EPCIS/", + "epcis": "https://gs1.github.io/EPCIS/", + "cbv": "https://ref.gs1.org/cbv/", + "type": "@type", + "id": "@id" + }, + "type": "EPCISDocument", + "schemaVersion": "2.0", + "creationDate": "2024-03-01T15:00:00Z", + "epcisBody": { + "eventList": [{ + "type": "ObjectEvent", + "eventTime": "2024-03-01T15:00:00.000Z", + "eventTimeZoneOffset": "+00:00", + "epcList": ["urn:epc:id:sgtin:4012345.099999.9001"], + "action": "OBSERVE", + "bizStep": "https://ref.gs1.org/cbv/BizStep-inspecting", + "disposition": "https://ref.gs1.org/cbv/Disp-conformant", + "readPoint": {"id": "urn:epc:id:sgln:4012345.00002.0"}, + "bizLocation": {"id": "urn:epc:id:sgln:4012345.00002.0"} + }] + } + }, + "publishOptions": { + "privacy": "private", + "epochs": 12 + } + } + }, + { + "name": "Event 8: Pack Bicycle", + "description": "πŸ“¦ AGGREGATION: Bicycle packed onto shipping pallet", + "request": { + "epcisDocument": { + "@context": { + "@vocab": "https://gs1.github.io/EPCIS/", + "epcis": "https://gs1.github.io/EPCIS/", + "cbv": "https://ref.gs1.org/cbv/", + "type": "@type", + "id": "@id" + }, + "type": "EPCISDocument", + "schemaVersion": "2.0", + "creationDate": "2024-03-01T16:00:00Z", + "epcisBody": { + "eventList": [{ + "type": "AggregationEvent", + "eventTime": "2024-03-01T16:00:00.000Z", + "eventTimeZoneOffset": "+00:00", + "parentID": "urn:epc:id:sscc:4012345.0000000001", + "childEPCs": [ + "urn:epc:id:sgtin:4012345.099999.9001" + ], + "action": "ADD", + "bizStep": "https://ref.gs1.org/cbv/BizStep-packing", + "disposition": "https://ref.gs1.org/cbv/Disp-in_transit", + "readPoint": {"id": "urn:epc:id:sgln:4012345.00004.0"}, + "bizLocation": {"id": "urn:epc:id:sgln:4012345.00004.0"} + }] + } + }, + "publishOptions": { + "privacy": "private", + "epochs": 12 + } + } + }, + { + "name": "Event 9: Ship Pallet", + "description": "🚚 Pallet shipped to customer", + "request": { + "epcisDocument": { + "@context": { + "@vocab": "https://gs1.github.io/EPCIS/", + "epcis": "https://gs1.github.io/EPCIS/", + "cbv": "https://ref.gs1.org/cbv/", + "type": "@type", + "id": "@id" + }, + "type": "EPCISDocument", + "schemaVersion": "2.0", + "creationDate": "2024-03-02T08:00:00Z", + "epcisBody": { + "eventList": [{ + "type": "ObjectEvent", + "eventTime": "2024-03-02T08:00:00.000Z", + "eventTimeZoneOffset": "+00:00", + "epcList": ["urn:epc:id:sscc:4012345.0000000001"], + "action": "OBSERVE", + "bizStep": "https://ref.gs1.org/cbv/BizStep-shipping", + "disposition": "https://ref.gs1.org/cbv/Disp-in_transit", + "readPoint": {"id": "urn:epc:id:sgln:4012345.00005.0"}, + "bizLocation": {"id": "urn:epc:id:sgln:4012345.00005.0"}, + "bizTransactionList": [ + {"type": "https://ref.gs1.org/cbv/BTT-desadv", "bizTransaction": "urn:epc:id:gdti:4012345.00001.ASN-2024-001"} + ] + }] + } + }, + "publishOptions": { + "privacy": "private", + "epochs": 12 + } + } + } + ], + + "testQueries": { + "trackFrame": { + "description": "Track the carbon frame from receiving through assembly", + "epc": "urn:epc:id:sgtin:4012345.011111.1001", + "fullTrace": true, + "expectedEvents": ["receiving", "inspecting", "assembling (as input)"] + }, + "trackBicycle": { + "description": "Track the finished bicycle from creation to shipping", + "epc": "urn:epc:id:sgtin:4012345.099999.9001", + "fullTrace": true, + "expectedEvents": ["assembling (as output)", "inspecting", "packing (as child)", "shipping"] + }, + "findAllReceiving": { + "description": "Find all items received on March 1st", + "bizStep": "receiving", + "from": "2024-03-01T00:00:00Z", + "to": "2024-03-01T23:59:59Z" + }, + "findAssemblyEvents": { + "description": "Find all assembly operations", + "bizStep": "assembling" + }, + "findAtQualityLab": { + "description": "Find all events at the quality lab", + "bizLocation": "urn:epc:id:sgln:4012345.00002.0" + } + } +} diff --git a/packages/plugin-epcis/test-data/load-test-events.sh b/packages/plugin-epcis/test-data/load-test-events.sh new file mode 100755 index 0000000..8a70f53 --- /dev/null +++ b/packages/plugin-epcis/test-data/load-test-events.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# Load Bicycle Manufacturing Story - EPCIS Test Events +# Usage: ./load-test-events.sh [BASE_URL] + +BASE_URL="${1:-http://localhost:9200}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +DATA_FILE="$SCRIPT_DIR/bicycle-manufacturing-story.json" + +echo "🚴 Alpine Cycles - Bicycle Manufacturing Story" +echo "================================================" +echo "Loading EPCIS events to: $BASE_URL" +echo "" + +# Check if jq is available +if ! command -v jq &> /dev/null; then + echo "❌ jq is required but not installed. Install with: apt install jq" + exit 1 +fi + +# Load each event +EVENT_COUNT=$(jq '.events | length' "$DATA_FILE") +echo "πŸ“¦ Found $EVENT_COUNT events to load" +echo "" + +for i in $(seq 0 $((EVENT_COUNT - 1))); do + EVENT_NAME=$(jq -r ".events[$i].name" "$DATA_FILE") + EVENT_DESC=$(jq -r ".events[$i].description" "$DATA_FILE") + + echo "[$((i + 1))/$EVENT_COUNT] $EVENT_NAME" + echo " $EVENT_DESC" + + # Extract and send the request (contains epcisDocument + publishOptions) + REQUEST=$(jq ".events[$i].request" "$DATA_FILE") + + RESPONSE=$(curl -s -X POST "$BASE_URL/epcis/capture" \ + -H "Content-Type: application/json" \ + -d "$REQUEST") + + CAPTURE_ID=$(echo "$RESPONSE" | jq -r '.captureID // "error"') + STATUS=$(echo "$RESPONSE" | jq -r '.status // "error"') + + if [ "$CAPTURE_ID" != "error" ] && [ "$CAPTURE_ID" != "null" ]; then + echo " βœ… Captured: ID=$CAPTURE_ID" + else + echo " ❌ Failed: $RESPONSE" + fi + echo "" + + # Small delay to avoid overwhelming the server + sleep 0.5 +done + +echo "================================================" +echo "βœ… All events loaded!" +echo "" +echo "πŸ” Test Queries to Try:" +echo "" +echo "1. Track the carbon frame through assembly:" +echo " curl '$BASE_URL/epcis/events?epc=urn:epc:id:sgtin:4012345.011111.1001&fullTrace=true'" +echo "" +echo "2. Track the finished bicycle:" +echo " curl '$BASE_URL/epcis/events?epc=urn:epc:id:sgtin:4012345.099999.9001&fullTrace=true'" +echo "" +echo "3. Find all receiving events:" +echo " curl '$BASE_URL/epcis/events?bizStep=receiving'" +echo "" +echo "4. Find assembly operations:" +echo " curl '$BASE_URL/epcis/events?bizStep=assembling'" +echo "" +echo "5. Find events at quality lab:" +echo " curl '$BASE_URL/epcis/events?bizLocation=urn:epc:id:sgln:4012345.00002.0'" +echo "" + diff --git a/packages/plugin-epcis/tests/plugin-epcis.spec.ts b/packages/plugin-epcis/tests/plugin-epcis.spec.ts new file mode 100644 index 0000000..f1e5e67 --- /dev/null +++ b/packages/plugin-epcis/tests/plugin-epcis.spec.ts @@ -0,0 +1,80 @@ +import { describe, it, beforeEach, afterEach } from "mocha"; +import { expect } from "chai"; +import sinon from "sinon"; +import pluginEpcisPlugin from "../dist/index.js"; +import { + createExpressApp, + createInMemoryBlobStorage, + createMcpServerClientPair, + createMockDkgClient, +} from "@dkg/plugins/testing"; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import express from "express"; +// import request from "supertest"; + +// Mock DKG context +const mockDkgContext = { + dkg: createMockDkgClient(), + blob: createInMemoryBlobStorage(), +}; + +describe("@dkg/plugin-epcis checks", function () { + let mockMcpServer: McpServer; + let mockMcpClient: Client; + let apiRouter: express.Router; + let app: express.Application; + + this.timeout(5000); + + beforeEach(async () => { + const { server, client, connect } = await createMcpServerClientPair(); + mockMcpServer = server; + mockMcpClient = client; + apiRouter = express.Router(); + app = createExpressApp(); + + // Initialize plugin + pluginEpcisPlugin(mockDkgContext, mockMcpServer, apiRouter); + await connect(); + app.use("/", apiRouter); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe("Plugin Configuration", () => { + it("should create plugin without errors", () => { + expect(pluginEpcisPlugin).to.be.a("function"); + }); + }); + + describe("Core Functionality", () => { + it("should register tools or endpoints", async () => { + // TODO: Replace this placeholder with your actual tests! + // Example for MCP tools: + // const tools = await mockMcpClient.listTools().then((r) => r.tools); + // expect(tools.some((t) => t.name === "your-tool-name")).to.equal(true); + + // Example for API endpoints: + // request(app).get("/your-endpoint").expect(200); + + throw new Error( + "TODO: Replace placeholder test with your actual plugin functionality tests", + ); + }); + }); + + describe("Error Handling", () => { + it("should handle invalid parameters", async () => { + // TODO: Replace this placeholder with your actual error handling tests! + // Example: + // await request(app).get("/invalid-endpoint").expect(400); + + throw new Error( + "TODO: Replace placeholder test with your actual error handling tests", + ); + }); + }); +}); diff --git a/packages/plugin-epcis/tsconfig.json b/packages/plugin-epcis/tsconfig.json new file mode 100644 index 0000000..aff3847 --- /dev/null +++ b/packages/plugin-epcis/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@dkg/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +}