From 6b32eebfcc4b9ddf5ac9d1061e702715d57bd564 Mon Sep 17 00:00:00 2001 From: Vujkovic Date: Tue, 13 Jan 2026 20:49:20 +0100 Subject: [PATCH 1/5] epcis plugin V1 --- package-lock.json | 96 + packages/plugin-epcis/eslint.config.mjs | 4 + packages/plugin-epcis/package.json | 26 + packages/plugin-epcis/src/index.ts | 248 ++ packages/plugin-epcis/src/model/types.ts | 50 + .../src/schemas/epcis-json-schema.json | 2352 +++++++++++++++++ .../src/services/EPCISQueryService.ts | 135 + .../src/services/EPCISValidationService.ts | 81 + .../plugin-epcis/tests/plugin-epcis.spec.ts | 80 + packages/plugin-epcis/tsconfig.json | 9 + 10 files changed, 3081 insertions(+) create mode 100644 packages/plugin-epcis/eslint.config.mjs create mode 100644 packages/plugin-epcis/package.json create mode 100644 packages/plugin-epcis/src/index.ts create mode 100644 packages/plugin-epcis/src/model/types.ts create mode 100644 packages/plugin-epcis/src/schemas/epcis-json-schema.json create mode 100644 packages/plugin-epcis/src/services/EPCISQueryService.ts create mode 100644 packages/plugin-epcis/src/services/EPCISValidationService.ts create mode 100644 packages/plugin-epcis/tests/plugin-epcis.spec.ts create mode 100644 packages/plugin-epcis/tsconfig.json 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/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..a55a3c1 --- /dev/null +++ b/packages/plugin-epcis/src/index.ts @@ -0,0 +1,248 @@ +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"; + +// Helper function to send JSON-LD to publisher +async function sendToPublisher( + jsonLd: any, + metadata?: { source?: string; sourceId?: string } +): Promise<{ id: number; status: string; attemptCount: number }> { + const publisherUrl = process.env.PUBLISHER_URL || "http://localhost:9200"; + + 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: "private", + epochs: 2, + }, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Publisher request failed"); + } + + return response.json(); +} + +export default defineDkgPlugin((ctx, mcp, api) => { + + const validationService = new EpcisValidationService(); + const queryService = new EpcisQueryService(); + + console.log("🚀 EPCIS Plugin loaded"); + + // 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({}).passthrough().openapi({ + description: "EPCISDocument (JSON-LD)", + }), + response: { + description: "Capture accepted", + schema: z.object({ + status: z.string(), + receivedAt: z.string(), + captureID: z.string(), + eventCount: z.number(), + }), + }, + }, + async (req, res) => { + try { + const document = req.body; + + // Validate the EPCIS document + + const validation = validationService.validate(document); + + if (!validation.valid) { + return res.status(400).json({ + error: "Invalid EPCISDocument", + details: validation.errors, + } as any); + } + + // Send to publisher + const result = await sendToPublisher(document, { + source: "EPCIS", + sourceId: `epcis-${Date.now()}`, + }); + + // 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"; + + // Query publisher for asset status + const response = await fetch(`${publisherUrl}/api/dkg/assets/status/${captureID}`); + + 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:kam:item:2224813", + }), + from: z.string().optional().openapi({ + description: "Start of time range (ISO 8601)", + example: "2024-01-01T00:00:00Z", + }), + to: z.string().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:kam:location:workcenter:W1006", + }), + ual: z.string().optional().openapi({ + description: "Get event by specific UAL", + }), + }), + 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 } = 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, + }); + + 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..4f921c6 --- /dev/null +++ b/packages/plugin-epcis/src/services/EPCISQueryService.ts @@ -0,0 +1,135 @@ +/** + * EPCIS Query Service + * Supports composite filtering - combine multiple filters in one query + */ + +// Namespace prefixes for EPCIS queries +const PREFIXES = ` +PREFIX epcis: +PREFIX kam: +PREFIX schema: +PREFIX xsd: +`; + +export interface EpcisQueryParams { + epc?: string; + from?: string; + to?: string; + bizStep?: string; + bizLocation?: string; + ual?: string; +} + +/** + * 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 (!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 + if (params.epc) { + wherePatterns.push(`?event epcis:epcList "${escapeSparql(params.epc)}" .`); + } 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 + if (params.from || params.to) { + wherePatterns.push('?event epcis:eventTime ?eventTime .'); + if (params.from && params.to) { + filterClauses.push( + `FILTER(STR(?eventTime) >= "${escapeSparql(params.from)}" && STR(?eventTime) <= "${escapeSparql(params.to)}")` + ); + } else if (params.from) { + filterClauses.push(`FILTER(STR(?eventTime) >= "${escapeSparql(params.from)}")`); + } else if (params.to) { + filterClauses.push(`FILTER(STR(?eventTime) <= "${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..8aadaa7 --- /dev/null +++ b/packages/plugin-epcis/src/services/EPCISValidationService.ts @@ -0,0 +1,81 @@ +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 { + // Check basic structure first + if (!document || typeof document !== "object") { + return { + valid: false, + errors: ["Document must be a valid JSON object"], + }; + } + + const doc = document as EPCISDocument; + + // Check for required type + if (doc.type !== "EPCISDocument") { + return { + valid: false, + errors: [`Invalid type: expected "EPCISDocument", got "${doc.type}"`], + }; + } + + // Get event list from either location + const eventList = doc.eventList || doc.epcisBody?.eventList; + + if (!eventList || !Array.isArray(eventList) || eventList.length === 0) { + return { + valid: false, + errors: ["EPCISDocument must contain at least one event in eventList or epcisBody.eventList"], + }; + } + + // Validate against GS1 schema + 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, + eventCount: eventList.length, + }; + } + + 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/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"] +} From 520ed35a2451864c7ebd6c361b2b168836644ec7 Mon Sep 17 00:00:00 2001 From: Vujkovic Date: Thu, 15 Jan 2026 21:24:26 +0100 Subject: [PATCH 2/5] bugbot relevant fixes --- packages/plugin-epcis/src/index.ts | 9 ++++++++- packages/plugin-epcis/src/services/EPCISQueryService.ts | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/plugin-epcis/src/index.ts b/packages/plugin-epcis/src/index.ts index a55a3c1..7c57dbf 100644 --- a/packages/plugin-epcis/src/index.ts +++ b/packages/plugin-epcis/src/index.ts @@ -131,8 +131,15 @@ export default defineDkgPlugin((ctx, mcp, api) => { const { captureID } = req.params; const publisherUrl = process.env.PUBLISHER_URL || "http://localhost:9200"; + const captureIdPattern = /^[0-9]+$/; + if (!captureIdPattern.test(captureID)) { + return res.status(400).json({ + error: "Invalid captureID format", + captureID, + } as any); + } // Query publisher for asset status - const response = await fetch(`${publisherUrl}/api/dkg/assets/status/${captureID}`); + const response = await fetch(`${publisherUrl}/api/dkg/assets/status/${encodeURIComponent(captureID)}`); if (!response.ok) { if (response.status === 404) { diff --git a/packages/plugin-epcis/src/services/EPCISQueryService.ts b/packages/plugin-epcis/src/services/EPCISQueryService.ts index 4f921c6..1e46a26 100644 --- a/packages/plugin-epcis/src/services/EPCISQueryService.ts +++ b/packages/plugin-epcis/src/services/EPCISQueryService.ts @@ -32,6 +32,11 @@ function escapeSparql(value: string): string { * 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}`; } From 89bdf60892d8364bef02319d21f8f677a840777b Mon Sep 17 00:00:00 2001 From: Vujkovic Date: Mon, 19 Jan 2026 22:04:51 +0100 Subject: [PATCH 3/5] added docs v1 --- .../docs/EPCIS-Integration-Guide.md | 612 ++++++++++++++++++ 1 file changed, 612 insertions(+) create mode 100644 packages/plugin-epcis/docs/EPCIS-Integration-Guide.md 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)* + From 3e0179f8959fe3d00dfb9fc81b6ef3a55809a94b Mon Sep 17 00:00:00 2001 From: Vujkovic Date: Tue, 20 Jan 2026 21:59:05 +0100 Subject: [PATCH 4/5] demo example extension --- packages/plugin-epcis/src/index.ts | 142 ++++++- .../src/services/EPCISQueryService.ts | 19 +- .../bicycle-manufacturing-story.json | 346 ++++++++++++++++++ .../test-data/load-test-events.sh | 73 ++++ 4 files changed, 575 insertions(+), 5 deletions(-) create mode 100644 packages/plugin-epcis/test-data/bicycle-manufacturing-story.json create mode 100755 packages/plugin-epcis/test-data/load-test-events.sh diff --git a/packages/plugin-epcis/src/index.ts b/packages/plugin-epcis/src/index.ts index 7c57dbf..3030ae8 100644 --- a/packages/plugin-epcis/src/index.ts +++ b/packages/plugin-epcis/src/index.ts @@ -39,6 +39,137 @@ export default defineDkgPlugin((ctx, mcp, api) => { 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), time range, 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("Start of time range (ISO 8601, e.g., 2024-01-01T00:00:00Z)"), + to: z.string().optional().describe("End of time range (ISO 8601)"), + 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?.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", @@ -183,7 +314,7 @@ export default defineDkgPlugin((ctx, mcp, api) => { query: z.object({ epc: z.string().optional().openapi({ description: "Filter by EPC (product identifier)", - example: "urn:kam:item:2224813", + example: "urn:epc:id:sgtin:0614141.107346.2017", }), from: z.string().optional().openapi({ description: "Start of time range (ISO 8601)", @@ -199,11 +330,15 @@ export default defineDkgPlugin((ctx, mcp, api) => { }), bizLocation: z.string().optional().openapi({ description: "Filter by business location", - example: "urn:kam:location:workcenter:W1006", + 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", @@ -217,7 +352,7 @@ export default defineDkgPlugin((ctx, mcp, api) => { }, async (req, res) => { try { - const { epc, from, to, bizStep, bizLocation, ual } = req.query; + const { epc, from, to, bizStep, bizLocation, ual, fullTrace } = req.query; // Build the SPARQL query based on parameters const sparqlQuery = queryService.buildQuery({ @@ -227,6 +362,7 @@ export default defineDkgPlugin((ctx, mcp, api) => { bizStep: bizStep as string, bizLocation: bizLocation as string, ual: ual as string, + fullTrace: fullTrace === 'true', }); console.log("[EPCIS Events] Executing SPARQL query:", sparqlQuery); diff --git a/packages/plugin-epcis/src/services/EPCISQueryService.ts b/packages/plugin-epcis/src/services/EPCISQueryService.ts index 1e46a26..0c3af7a 100644 --- a/packages/plugin-epcis/src/services/EPCISQueryService.ts +++ b/packages/plugin-epcis/src/services/EPCISQueryService.ts @@ -18,6 +18,8 @@ export interface EpcisQueryParams { bizStep?: string; bizLocation?: string; ual?: string; + /** If true, searches all EPC fields (epcList, inputEPCList, outputEPCList, childEPCs, parentID) */ + fullTrace?: boolean; } /** @@ -64,9 +66,22 @@ export class EpcisQueryService { // Filter by event type (must be EPCIS event) filterClauses.push('FILTER(STRSTARTS(STR(?eventType), "https://gs1.github.io/EPCIS/"))'); - // EPC filter + // EPC filter - with optional full traceability across all EPC fields if (params.epc) { - wherePatterns.push(`?event epcis:epcList "${escapeSparql(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 . }'); } 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..eb88cab --- /dev/null +++ b/packages/plugin-epcis/test-data/bicycle-manufacturing-story.json @@ -0,0 +1,346 @@ +{ + "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", + "document": { + "@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"} + ] + }] + } + } + }, + { + "name": "Event 2: Receive Wheels", + "description": "Front and rear wheels arrive from wheel supplier", + "document": { + "@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"} + ] + }] + } + } + }, + { + "name": "Event 3: Receive Handlebar", + "description": "Aluminum handlebar arrives", + "document": { + "@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"} + }] + } + } + }, + { + "name": "Event 4: Inspect Frame", + "description": "Quality check on carbon fiber frame - PASSED", + "document": { + "@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"} + }] + } + } + }, + { + "name": "Event 5: Inspect Wheels", + "description": "Quality check on both wheels - PASSED", + "document": { + "@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"} + }] + } + } + }, + { + "name": "Event 6: Assemble Bicycle", + "description": "🔧 TRANSFORMATION: Components assembled into finished bicycle", + "document": { + "@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"} + }] + } + } + }, + { + "name": "Event 7: Final Quality Check", + "description": "Finished bicycle passes final inspection", + "document": { + "@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"} + }] + } + } + }, + { + "name": "Event 8: Pack Bicycle", + "description": "📦 AGGREGATION: Bicycle packed onto shipping pallet", + "document": { + "@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"} + }] + } + } + }, + { + "name": "Event 9: Ship Pallet", + "description": "🚚 Pallet shipped to customer", + "document": { + "@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"} + ] + }] + } + } + } + ], + + "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..c7e41dc --- /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 document + DOCUMENT=$(jq ".events[$i].document" "$DATA_FILE") + + RESPONSE=$(curl -s -X POST "$BASE_URL/epcis/capture" \ + -H "Content-Type: application/json" \ + -d "$DOCUMENT") + + 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 "" + From 6a8e3f0165a478d22752e91f5f0f0850aeef8ee8 Mon Sep 17 00:00:00 2001 From: Vujkovic Date: Wed, 21 Jan 2026 18:49:12 +0100 Subject: [PATCH 5/5] PR fixes, part 1 --- packages/plugin-epcis/src/index.ts | 133 +++-- .../src/services/EPCISQueryService.ts | 19 +- .../src/services/EPCISValidationService.ts | 34 +- .../bicycle-manufacturing-story.json | 501 ++++++++++-------- .../test-data/load-test-events.sh | 6 +- 5 files changed, 382 insertions(+), 311 deletions(-) diff --git a/packages/plugin-epcis/src/index.ts b/packages/plugin-epcis/src/index.ts index 3030ae8..8c41c20 100644 --- a/packages/plugin-epcis/src/index.ts +++ b/packages/plugin-epcis/src/index.ts @@ -4,32 +4,48 @@ 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 } + 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"; - 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: "private", - epochs: 2, - }, - }), - }); + 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"); - } + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Publisher request failed"); + } - return response.json(); + 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) => { @@ -46,12 +62,12 @@ export default defineDkgPlugin((ctx, mcp, api) => { title: "Query EPCIS Events", description: "Query EPCIS supply chain events from the OriginTrail DKG. " + - "Can filter by EPC (product identifier), time range, business step, or location. " + + "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("Start of time range (ISO 8601, e.g., 2024-01-01T00:00:00Z)"), - to: z.string().optional().describe("End of time range (ISO 8601)"), + 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"), @@ -80,9 +96,9 @@ export default defineDkgPlugin((ctx, mcp, api) => { type: "text", text: JSON.stringify({ summary, - count: results?.length || 0, + count: results?.data.length || 0, events: results || [], - query: sparqlQuery, + //query: sparqlQuery, }, null, 2) } ], @@ -178,8 +194,20 @@ export default defineDkgPlugin((ctx, mcp, api) => { tag: "EPCIS", summary: "Capture EPCIS Document", description: "Accept an EPCISDocument and queue it for publishing to DKG", - body: z.object({}).passthrough().openapi({ - description: "EPCISDocument (JSON-LD)", + 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", @@ -193,11 +221,10 @@ export default defineDkgPlugin((ctx, mcp, api) => { }, async (req, res) => { try { - const document = req.body; + const { epcisDocument, publishOptions } = req.body; // Validate the EPCIS document - - const validation = validationService.validate(document); + const validation = validationService.validate(epcisDocument); if (!validation.valid) { return res.status(400).json({ @@ -206,11 +233,15 @@ export default defineDkgPlugin((ctx, mcp, api) => { } as any); } - // Send to publisher - const result = await sendToPublisher(document, { - source: "EPCIS", - sourceId: `epcis-${Date.now()}`, - }); + // 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 = { @@ -225,7 +256,7 @@ export default defineDkgPlugin((ctx, mcp, api) => { console.error("[EPCIS Capture] Error:", error); res.status(500).json({ error: "Failed to process capture", - message: error.message, + //message: error.message, } as any); } } @@ -262,7 +293,7 @@ export default defineDkgPlugin((ctx, mcp, api) => { const { captureID } = req.params; const publisherUrl = process.env.PUBLISHER_URL || "http://localhost:9200"; - const captureIdPattern = /^[0-9]+$/; + const captureIdPattern = /^[0-9]{1,20}$/; if (!captureIdPattern.test(captureID)) { return res.status(400).json({ error: "Invalid captureID format", @@ -270,7 +301,21 @@ export default defineDkgPlugin((ctx, mcp, api) => { } as any); } // Query publisher for asset status - const response = await fetch(`${publisherUrl}/api/dkg/assets/status/${encodeURIComponent(captureID)}`); + 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) { @@ -296,7 +341,7 @@ export default defineDkgPlugin((ctx, mcp, api) => { console.error("[EPCIS Status] Error:", error); res.status(500).json({ error: "Failed to get capture status", - message: error.message, + //message: error.message, } as any); } } @@ -316,11 +361,11 @@ export default defineDkgPlugin((ctx, mcp, api) => { description: "Filter by EPC (product identifier)", example: "urn:epc:id:sgtin:0614141.107346.2017", }), - from: z.string().optional().openapi({ + 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().optional().openapi({ + 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", }), @@ -332,9 +377,9 @@ export default defineDkgPlugin((ctx, mcp, api) => { description: "Filter by business location", example: "urn:epc:id:sgln:0614141.00001.0", }), - ual: z.string().optional().openapi({ + /*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", @@ -352,7 +397,7 @@ export default defineDkgPlugin((ctx, mcp, api) => { }, async (req, res) => { try { - const { epc, from, to, bizStep, bizLocation, ual, fullTrace } = req.query; + const { epc, from, to, bizStep, bizLocation, /*ual,*/ fullTrace } = req.query; // Build the SPARQL query based on parameters const sparqlQuery = queryService.buildQuery({ @@ -361,7 +406,7 @@ export default defineDkgPlugin((ctx, mcp, api) => { to: to as string, bizStep: bizStep as string, bizLocation: bizLocation as string, - ual: ual as string, + //ual: ual as string, fullTrace: fullTrace === 'true', }); @@ -372,7 +417,7 @@ export default defineDkgPlugin((ctx, mcp, api) => { res.json({ success: true, - query: sparqlQuery, + //query: sparqlQuery, results: results || [], count: results?.length || 0, }); diff --git a/packages/plugin-epcis/src/services/EPCISQueryService.ts b/packages/plugin-epcis/src/services/EPCISQueryService.ts index 0c3af7a..a867014 100644 --- a/packages/plugin-epcis/src/services/EPCISQueryService.ts +++ b/packages/plugin-epcis/src/services/EPCISQueryService.ts @@ -6,7 +6,6 @@ // Namespace prefixes for EPCIS queries const PREFIXES = ` PREFIX epcis: -PREFIX kam: PREFIX schema: PREFIX xsd: `; @@ -17,7 +16,7 @@ export interface EpcisQueryParams { to?: string; bizStep?: string; bizLocation?: string; - ual?: string; + // ual?: string; // TODO: Re-enable when UAL query is implemented /** If true, searches all EPC fields (epcList, inputEPCList, outputEPCList, childEPCs, parentID) */ fullTrace?: boolean; } @@ -52,9 +51,9 @@ export class EpcisQueryService { */ buildQuery(params: EpcisQueryParams): string { // Special case: UAL lookup returns all triples for that graph - if (params.ual) { + /*if (params.ual) { return this.getEventByUal(params.ual); - } + }*/ const wherePatterns: string[] = []; const filterClauses: string[] = []; @@ -102,17 +101,17 @@ export class EpcisQueryService { optionalClauses.push('OPTIONAL { ?event epcis:bizLocation ?bizLocation . }'); } - // Time range filter + // 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(STR(?eventTime) >= "${escapeSparql(params.from)}" && STR(?eventTime) <= "${escapeSparql(params.to)}")` + `FILTER(xsd:dateTime(?eventTime) >= xsd:dateTime("${escapeSparql(params.from)}") && xsd:dateTime(?eventTime) <= xsd:dateTime("${escapeSparql(params.to)}"))` ); } else if (params.from) { - filterClauses.push(`FILTER(STR(?eventTime) >= "${escapeSparql(params.from)}")`); + filterClauses.push(`FILTER(xsd:dateTime(?eventTime) >= xsd:dateTime("${escapeSparql(params.from)}"))`); } else if (params.to) { - filterClauses.push(`FILTER(STR(?eventTime) <= "${escapeSparql(params.to)}")`); + filterClauses.push(`FILTER(xsd:dateTime(?eventTime) <= xsd:dateTime("${escapeSparql(params.to)}"))`); } } else { optionalClauses.push('OPTIONAL { ?event epcis:eventTime ?eventTime . }'); @@ -139,7 +138,7 @@ LIMIT 100`; /** * Query event by UAL (get full event details) */ - private getEventByUal(ual: string): string { + /*private getEventByUal(ual: string): string { // Basic UAL format validation if (!ual.startsWith('did:')) { throw new Error('Invalid UAL format'); @@ -151,5 +150,5 @@ WHERE { ?subject ?predicate ?object . } }`; - } + }*/ } diff --git a/packages/plugin-epcis/src/services/EPCISValidationService.ts b/packages/plugin-epcis/src/services/EPCISValidationService.ts index 8aadaa7..db77cdd 100644 --- a/packages/plugin-epcis/src/services/EPCISValidationService.ts +++ b/packages/plugin-epcis/src/services/EPCISValidationService.ts @@ -23,35 +23,6 @@ export class EpcisValidationService { * Validate an EPCISDocument against the GS1 JSON Schema */ validate(document: unknown): ValidationResult { - // Check basic structure first - if (!document || typeof document !== "object") { - return { - valid: false, - errors: ["Document must be a valid JSON object"], - }; - } - - const doc = document as EPCISDocument; - - // Check for required type - if (doc.type !== "EPCISDocument") { - return { - valid: false, - errors: [`Invalid type: expected "EPCISDocument", got "${doc.type}"`], - }; - } - - // Get event list from either location - const eventList = doc.eventList || doc.epcisBody?.eventList; - - if (!eventList || !Array.isArray(eventList) || eventList.length === 0) { - return { - valid: false, - errors: ["EPCISDocument must contain at least one event in eventList or epcisBody.eventList"], - }; - } - - // Validate against GS1 schema const isValid = this.validateSchema(document); if (!isValid) { @@ -62,10 +33,13 @@ export class EpcisValidationService { return { valid: false, errors, - eventCount: eventList.length, }; } + // Count events for response + const doc = document as EPCISDocument; + const eventList = doc.epcisBody?.eventList || []; + return { valid: true, eventCount: eventList.length, diff --git a/packages/plugin-epcis/test-data/bicycle-manufacturing-story.json b/packages/plugin-epcis/test-data/bicycle-manufacturing-story.json index eb88cab..dcfe3ca 100644 --- a/packages/plugin-epcis/test-data/bicycle-manufacturing-story.json +++ b/packages/plugin-epcis/test-data/bicycle-manufacturing-story.json @@ -29,286 +29,340 @@ { "name": "Event 1: Receive Frame", "description": "Carbon fiber frame arrives from supplier", - "document": { - "@context": { - "@vocab": "https://gs1.github.io/EPCIS/", - "epcis": "https://gs1.github.io/EPCIS/", - "cbv": "https://ref.gs1.org/cbv/", - "type": "@type", - "id": "@id" + "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"} + ] + }] + } }, - "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", - "document": { - "@context": { - "@vocab": "https://gs1.github.io/EPCIS/", - "epcis": "https://gs1.github.io/EPCIS/", - "cbv": "https://ref.gs1.org/cbv/", - "type": "@type", - "id": "@id" + "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"} + ] + }] + } }, - "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", - "document": { - "@context": { - "@vocab": "https://gs1.github.io/EPCIS/", - "epcis": "https://gs1.github.io/EPCIS/", - "cbv": "https://ref.gs1.org/cbv/", - "type": "@type", - "id": "@id" + "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"} + }] + } }, - "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", - "document": { - "@context": { - "@vocab": "https://gs1.github.io/EPCIS/", - "epcis": "https://gs1.github.io/EPCIS/", - "cbv": "https://ref.gs1.org/cbv/", - "type": "@type", - "id": "@id" + "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"} + }] + } }, - "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", - "document": { - "@context": { - "@vocab": "https://gs1.github.io/EPCIS/", - "epcis": "https://gs1.github.io/EPCIS/", - "cbv": "https://ref.gs1.org/cbv/", - "type": "@type", - "id": "@id" + "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"} + }] + } }, - "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", - "document": { - "@context": { - "@vocab": "https://gs1.github.io/EPCIS/", - "epcis": "https://gs1.github.io/EPCIS/", - "cbv": "https://ref.gs1.org/cbv/", - "type": "@type", - "id": "@id" + "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"} + }] + } }, - "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", - "document": { - "@context": { - "@vocab": "https://gs1.github.io/EPCIS/", - "epcis": "https://gs1.github.io/EPCIS/", - "cbv": "https://ref.gs1.org/cbv/", - "type": "@type", - "id": "@id" + "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"} + }] + } }, - "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", - "document": { - "@context": { - "@vocab": "https://gs1.github.io/EPCIS/", - "epcis": "https://gs1.github.io/EPCIS/", - "cbv": "https://ref.gs1.org/cbv/", - "type": "@type", - "id": "@id" + "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"} + }] + } }, - "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", - "document": { - "@context": { - "@vocab": "https://gs1.github.io/EPCIS/", - "epcis": "https://gs1.github.io/EPCIS/", - "cbv": "https://ref.gs1.org/cbv/", - "type": "@type", - "id": "@id" + "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"} + ] + }] + } }, - "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 } } } @@ -343,4 +397,3 @@ } } } - diff --git a/packages/plugin-epcis/test-data/load-test-events.sh b/packages/plugin-epcis/test-data/load-test-events.sh index c7e41dc..8a70f53 100755 --- a/packages/plugin-epcis/test-data/load-test-events.sh +++ b/packages/plugin-epcis/test-data/load-test-events.sh @@ -29,12 +29,12 @@ for i in $(seq 0 $((EVENT_COUNT - 1))); do echo "[$((i + 1))/$EVENT_COUNT] $EVENT_NAME" echo " $EVENT_DESC" - # Extract and send the document - DOCUMENT=$(jq ".events[$i].document" "$DATA_FILE") + # 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 "$DOCUMENT") + -d "$REQUEST") CAPTURE_ID=$(echo "$RESPONSE" | jq -r '.captureID // "error"') STATUS=$(echo "$RESPONSE" | jq -r '.status // "error"')