{
  "info": {
    "_postman_id": "g2i2-graphql-collection",
    "name": "G2I2 GraphQL Gateway",
    "description": "Live Haramain (HHR) train data over GraphQL.\n\n• Queries & mutations: HTTP POST to {{graphqlUrl}} ({{baseUrl}}/graphql).\n• Subscriptions: WebSocket to {{wsUrl}} using the graphql-transport-ws subprotocol.\n\nSelect an environment (Local / Production / Staging) before sending. All inputs (lineId, sectionId, train numbers …) are environment/collection variables — edit them in one place.\n\nAuth: every operation needs a bearer token. Run Auth → Login first (set the username/password variables to your credentials); it saves {{accessToken}}, which the whole collection sends automatically.\n\nNote: live subscriptions are gated to 'coming soon' in production; use the Staging or Local environment to run them.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{accessToken}}",
        "type": "string"
      }
    ]
  },
  "item": [
    {
      "name": "Auth",
      "description": "Obtain a bearer token. Run Login first — its test script saves accessToken to the active environment, and every other request sends it automatically. Set the username/password variables to the credentials your provider issued.",
      "item": [
        {
          "name": "login",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "graphql",
              "graphql": {
                "query": "mutation Login($username: String!, $password: String!) {\n  login(username: $username, password: $password) {\n    accessToken\n    tokenType\n    expiresIn\n    username\n    role\n  }\n}",
                "variables": "{\n  \"username\": \"{{username}}\",\n  \"password\": \"{{password}}\"\n}"
              }
            },
            "url": {
              "raw": "{{graphqlUrl}}",
              "host": [
                "{{graphqlUrl}}"
              ]
            },
            "description": "Exchange username + password for a JWT. The token is saved to the {{accessToken}} variable by this request's test script.",
            "auth": {
              "type": "noauth"
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Save the token so the rest of the collection is authenticated.",
                  "const d = pm.response.json().data;",
                  "if (d && d.login && d.login.accessToken) {",
                  "  pm.environment.set('accessToken', d.login.accessToken);",
                  "  pm.collectionVariables.set('accessToken', d.login.accessToken);",
                  "  console.log('Saved accessToken for', d.login.username, '(' + d.login.role + ')');",
                  "} else {",
                  "  console.warn('Login did not return a token:', JSON.stringify(pm.response.json()));",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "me",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "graphql",
              "graphql": {
                "query": "query Me {\n  me { username role }\n}",
                "variables": "{}"
              }
            },
            "url": {
              "raw": "{{graphqlUrl}}",
              "host": [
                "{{graphqlUrl}}"
              ]
            },
            "description": "Check who the current token belongs to (null if unauthenticated)."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Queries",
      "description": "Read operations (HTTP POST).",
      "item": [
        {
          "name": "lines",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "graphql",
              "graphql": {
                "query": "query Lines {\n  lines {\n    id\n    name\n    ready\n    hasTrains\n    hasMesh\n    lastSeen\n    sections { id name }\n  }\n}",
                "variables": "{}"
              }
            },
            "url": {
              "raw": "{{graphqlUrl}}",
              "host": [
                "{{graphqlUrl}}"
              ]
            },
            "description": "Discover every line (live from the upstream feed) with its sections, status and live flags."
          },
          "response": []
        },
        {
          "name": "trains",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "graphql",
              "graphql": {
                "query": "query Trains($lineId: ID!) {\n  trains(lineId: $lineId) {\n    id\n    status\n    km\n    direction\n    delay\n    departureStation\n    nextStation\n    nextStationEta\n    finalStation\n    currentEvent { node type actualTime }\n    nextEvent { node type plannedTime forecastTime diff }\n  }\n}",
                "variables": "{\n  \"lineId\": \"{{lineId}}\"\n}"
              }
            },
            "url": {
              "raw": "{{graphqlUrl}}",
              "host": [
                "{{graphqlUrl}}"
              ]
            },
            "description": "Aggregated live roster of every current train on a line (the whole fleet, merged from the realtime feed). currentEvent / nextEvent show where each train is now and the next node it will reach with its ETA."
          },
          "response": []
        },
        {
          "name": "searchTrains",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "graphql",
              "graphql": {
                "query": "query SearchTrains($query: String!) {\n  searchTrains(query: $query) {\n    id\n    status\n    km\n    direction\n    delay\n    origin\n    destination\n    departureTime\n    arrivalTime\n  }\n}",
                "variables": "{\n  \"query\": \"{{searchQuery}}\"\n}"
              }
            },
            "url": {
              "raw": "{{graphqlUrl}}",
              "host": [
                "{{graphqlUrl}}"
              ]
            },
            "description": "Search trains by number or wildcard (use % to match all). Returns matching trains."
          },
          "response": []
        },
        {
          "name": "lineSections",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "graphql",
              "graphql": {
                "query": "query LineSections($lineId: ID!) {\n  lineSections(lineId: $lineId) { id name }\n}",
                "variables": "{\n  \"lineId\": \"{{lineId}}\"\n}"
              }
            },
            "url": {
              "raw": "{{graphqlUrl}}",
              "host": [
                "{{graphqlUrl}}"
              ]
            },
            "description": "The sections (bands) that make up a line."
          },
          "response": []
        },
        {
          "name": "sectionPoints",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "graphql",
              "graphql": {
                "query": "query SectionPoints($lineId: ID!, $sectionId: ID!, $sectionText: String!) {\n  sectionPoints(lineId: $lineId, sectionId: $sectionId, sectionText: $sectionText) {\n    id\n    name\n    pk\n  }\n}",
                "variables": "{\n  \"lineId\": \"{{lineId}}\",\n  \"sectionId\": \"{{sectionId}}\",\n  \"sectionText\": \"{{sectionText}}\"\n}"
              }
            },
            "url": {
              "raw": "{{graphqlUrl}}",
              "host": [
                "{{graphqlUrl}}"
              ]
            },
            "description": "Every timing point of a section — stations, sidings, crossovers, links."
          },
          "response": []
        },
        {
          "name": "sectionTrains",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "graphql",
              "graphql": {
                "query": "query SectionTrains(\n  $lineId: ID!\n  $sectionId: ID!\n  $sectionText: String!\n  $prevDate: String!\n  $nextDate: String!\n  $currentDate: String!\n) {\n  sectionTrains(\n    lineId: $lineId\n    sectionId: $sectionId\n    sectionText: $sectionText\n    prevDate: $prevDate\n    nextDate: $nextDate\n    currentDate: $currentDate\n  ) {\n    systemOk\n    circulations {\n      trainId\n      status\n      points { pointName time delay }\n    }\n  }\n}",
                "variables": "{\n  \"lineId\": \"{{lineId}}\",\n  \"sectionId\": \"{{sectionId}}\",\n  \"sectionText\": \"{{sectionText}}\",\n  \"prevDate\": \"{{prevDate}}\",\n  \"nextDate\": \"{{nextDate}}\",\n  \"currentDate\": \"{{nowMs}}\"\n}"
              }
            },
            "url": {
              "raw": "{{graphqlUrl}}",
              "host": [
                "{{graphqlUrl}}"
              ]
            },
            "description": "Timetable: circulations on a section over a time window, with per-point times & delays. The prevDate / nextDate / currentDate epoch-ms variables are set automatically by this request's pre-request script (a ±2h window around now)."
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Set the epoch-millisecond time variables used by this query.",
                  "const now = Date.now();",
                  "pm.variables.set('nowMs', String(now));",
                  "pm.variables.set('prevDate', String(now - 2 * 3600 * 1000));",
                  "pm.variables.set('nextDate', String(now + 2 * 3600 * 1000));"
                ]
              }
            }
          ]
        },
        {
          "name": "trainDetail",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "graphql",
              "graphql": {
                "query": "query TrainDetail(\n  $lineId: ID!\n  $baseNumber: String!\n  $serviceDate: String!\n  $currentDate: String\n) {\n  trainDetail(\n    lineId: $lineId\n    baseNumber: $baseNumber\n    serviceDate: $serviceDate\n    currentDate: $currentDate\n  ) {\n    circulationNumber\n    circulationDate\n    serviceType\n    company\n    operator\n    events { node type actualTrack plannedTime actualTime diff }\n  }\n}",
                "variables": "{\n  \"lineId\": \"{{lineId}}\",\n  \"baseNumber\": \"{{baseNumber}}\",\n  \"serviceDate\": \"{{nowMs}}\",\n  \"currentDate\": \"{{nowMs}}\"\n}"
              }
            },
            "url": {
              "raw": "{{graphqlUrl}}",
              "host": [
                "{{graphqlUrl}}"
              ]
            },
            "description": "Full movement history of one circulation — every node, planned vs actual. The serviceDate / currentDate epoch-ms variables are set automatically by this request's pre-request script."
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Set the epoch-millisecond time variables used by this query.",
                  "const now = Date.now();",
                  "pm.variables.set('nowMs', String(now));",
                  "pm.variables.set('prevDate', String(now - 2 * 3600 * 1000));",
                  "pm.variables.set('nextDate', String(now + 2 * 3600 * 1000));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Mutations",
      "description": "Write operations (HTTP POST). These affect live operator viewers — use with care.",
      "item": [
        {
          "name": "centerTrain",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "graphql",
              "graphql": {
                "query": "mutation CenterTrain($idTrain: ID!, $viewer: String) {\n  centerTrain(idTrain: $idTrain, viewer: $viewer)\n}",
                "variables": "{\n  \"idTrain\": \"{{trainId}}\",\n  \"viewer\": \"{{viewer}}\"\n}"
              }
            },
            "url": {
              "raw": "{{graphqlUrl}}",
              "host": [
                "{{graphqlUrl}}"
              ]
            },
            "description": "Center & zoom an operator viewer on a train. ⚠️ Affects live operator viewers in the control room."
          },
          "response": []
        },
        {
          "name": "centerElement",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "graphql",
              "graphql": {
                "query": "mutation CenterElement($element: ID!, $line: ID!, $viewer: String) {\n  centerElement(element: $element, line: $line, viewer: $viewer)\n}",
                "variables": "{\n  \"element\": \"{{elementId}}\",\n  \"line\": \"{{lineId}}\",\n  \"viewer\": \"{{viewer}}\"\n}"
              }
            },
            "url": {
              "raw": "{{graphqlUrl}}",
              "host": [
                "{{graphqlUrl}}"
              ]
            },
            "description": "Center an operator viewer on a mesh element. ⚠️ Affects live operator viewers in the control room."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Subscriptions (WebSocket)",
      "description": "Live streams over WebSocket (graphql-transport-ws). Gated to 'coming soon' in production — run them against Local or Staging.",
      "item": [
        {
          "name": "trainPositions (live)",
          "request": {
            "url": {
              "raw": "{{wsUrl}}",
              "host": [
                "{{wsUrl}}"
              ]
            },
            "method": "GET",
            "header": [],
            "description": "Live trains as they update. Prefer the station view (departureStation, nextStation + nextStationEta, finalStation) over the noisy km/circuit fields.\n\nHow to run in Postman:\n1. Run Auth -> Login first so {{accessToken}} is set.\n2. Open this WebSocket request and click Connect.\n3. In the Messages box, the graphql-transport-ws handshake is pre-saved:\n   first send the connection_init frame (carries your token), then the subscribe frame.\n4. Live frames stream into the Messages panel.\n\nconnection_init frame:\n{\"type\":\"connection_init\",\"payload\":{\"authorization\":\"Bearer {{accessToken}}\"}}\n\nsubscribe frame:\n{\"id\":\"1\",\"type\":\"subscribe\",\"payload\":{\"query\":\"subscription TrainPositions($lineId: ID!) {\\n  trainPositions(lineId: $lineId) {\\n    lineId\\n    trains {\\n      id\\n      status\\n      departureStation\\n      nextStation\\n      nextStationEta\\n      finalStation\\n    }\\n  }\\n}\"}}"
          },
          "protocolProfileBehavior": {
            "disableBodyPruning": true
          },
          "response": [],
          "messages": [
            {
              "name": "1 · connection_init",
              "content": "{\"type\":\"connection_init\",\"payload\":{\"authorization\":\"Bearer {{accessToken}}\"}}"
            },
            {
              "name": "2 · subscribe",
              "content": "{\"id\":\"1\",\"type\":\"subscribe\",\"payload\":{\"query\":\"subscription TrainPositions($lineId: ID!) {\\n  trainPositions(lineId: $lineId) {\\n    lineId\\n    trains {\\n      id\\n      status\\n      departureStation\\n      nextStation\\n      nextStationEta\\n      finalStation\\n    }\\n  }\\n}\"}}"
            }
          ]
        },
        {
          "name": "sinopticElements (live)",
          "request": {
            "url": {
              "raw": "{{wsUrl}}",
              "host": [
                "{{wsUrl}}"
              ]
            },
            "method": "GET",
            "header": [],
            "description": "Live signalling mesh — circuits, signals, switches, blocks and their states.\n\nHow to run in Postman:\n1. Run Auth -> Login first so {{accessToken}} is set.\n2. Open this WebSocket request and click Connect.\n3. In the Messages box, the graphql-transport-ws handshake is pre-saved:\n   first send the connection_init frame (carries your token), then the subscribe frame.\n4. Live frames stream into the Messages panel.\n\nconnection_init frame:\n{\"type\":\"connection_init\",\"payload\":{\"authorization\":\"Bearer {{accessToken}}\"}}\n\nsubscribe frame:\n{\"id\":\"1\",\"type\":\"subscribe\",\"payload\":{\"query\":\"subscription SinopticElements($lineId: ID!) {\\n  sinopticElements(lineId: $lineId) {\\n    lineId\\n    items { id type event parts { id state } }\\n  }\\n}\"}}"
          },
          "protocolProfileBehavior": {
            "disableBodyPruning": true
          },
          "response": [],
          "messages": [
            {
              "name": "1 · connection_init",
              "content": "{\"type\":\"connection_init\",\"payload\":{\"authorization\":\"Bearer {{accessToken}}\"}}"
            },
            {
              "name": "2 · subscribe",
              "content": "{\"id\":\"1\",\"type\":\"subscribe\",\"payload\":{\"query\":\"subscription SinopticElements($lineId: ID!) {\\n  sinopticElements(lineId: $lineId) {\\n    lineId\\n    items { id type event parts { id state } }\\n  }\\n}\"}}"
            }
          ]
        },
        {
          "name": "lines (live)",
          "request": {
            "url": {
              "raw": "{{wsUrl}}",
              "host": [
                "{{wsUrl}}"
              ]
            },
            "method": "GET",
            "header": [],
            "description": "Emits the full line list whenever a new line is discovered.\n\nHow to run in Postman:\n1. Run Auth -> Login first so {{accessToken}} is set.\n2. Open this WebSocket request and click Connect.\n3. In the Messages box, the graphql-transport-ws handshake is pre-saved:\n   first send the connection_init frame (carries your token), then the subscribe frame.\n4. Live frames stream into the Messages panel.\n\nconnection_init frame:\n{\"type\":\"connection_init\",\"payload\":{\"authorization\":\"Bearer {{accessToken}}\"}}\n\nsubscribe frame:\n{\"id\":\"1\",\"type\":\"subscribe\",\"payload\":{\"query\":\"subscription Lines {\\n  lines { id name ready hasTrains hasMesh }\\n}\"}}"
          },
          "protocolProfileBehavior": {
            "disableBodyPruning": true
          },
          "response": [],
          "messages": [
            {
              "name": "1 · connection_init",
              "content": "{\"type\":\"connection_init\",\"payload\":{\"authorization\":\"Bearer {{accessToken}}\"}}"
            },
            {
              "name": "2 · subscribe",
              "content": "{\"id\":\"1\",\"type\":\"subscribe\",\"payload\":{\"query\":\"subscription Lines {\\n  lines { id name ready hasTrains hasMesh }\\n}\"}}"
            }
          ]
        },
        {
          "name": "lineStatus (live)",
          "request": {
            "url": {
              "raw": "{{wsUrl}}",
              "host": [
                "{{wsUrl}}"
              ]
            },
            "method": "GET",
            "header": [],
            "description": "Per-line connection status changes.\n\nHow to run in Postman:\n1. Run Auth -> Login first so {{accessToken}} is set.\n2. Open this WebSocket request and click Connect.\n3. In the Messages box, the graphql-transport-ws handshake is pre-saved:\n   first send the connection_init frame (carries your token), then the subscribe frame.\n4. Live frames stream into the Messages panel.\n\nconnection_init frame:\n{\"type\":\"connection_init\",\"payload\":{\"authorization\":\"Bearer {{accessToken}}\"}}\n\nsubscribe frame:\n{\"id\":\"1\",\"type\":\"subscribe\",\"payload\":{\"query\":\"subscription LineStatus {\\n  lineStatus { id ready }\\n}\"}}"
          },
          "protocolProfileBehavior": {
            "disableBodyPruning": true
          },
          "response": [],
          "messages": [
            {
              "name": "1 · connection_init",
              "content": "{\"type\":\"connection_init\",\"payload\":{\"authorization\":\"Bearer {{accessToken}}\"}}"
            },
            {
              "name": "2 · subscribe",
              "content": "{\"id\":\"1\",\"type\":\"subscribe\",\"payload\":{\"query\":\"subscription LineStatus {\\n  lineStatus { id ready }\\n}\"}}"
            }
          ]
        }
      ]
    }
  ],
  "variable": [
    {
      "key": "graphqlUrl",
      "value": "{{baseUrl}}/graphql",
      "type": "string"
    },
    {
      "key": "username",
      "value": "demo",
      "type": "string"
    },
    {
      "key": "password",
      "value": "demo",
      "type": "string"
    },
    {
      "key": "accessToken",
      "value": "",
      "type": "string"
    },
    {
      "key": "lineId",
      "value": "HHR",
      "type": "string"
    },
    {
      "key": "sectionId",
      "value": "MK-KA-MD",
      "type": "string"
    },
    {
      "key": "sectionText",
      "value": "MAKKAH - KAIA - MADINAH",
      "type": "string"
    },
    {
      "key": "baseNumber",
      "value": "00162",
      "type": "string"
    },
    {
      "key": "trainId",
      "value": "00162",
      "type": "string"
    },
    {
      "key": "elementId",
      "value": "CV.HHR.C22.E1-C22O",
      "type": "string"
    },
    {
      "key": "viewer",
      "value": "SINOPTIC",
      "type": "string"
    },
    {
      "key": "searchQuery",
      "value": "00160",
      "type": "string"
    }
  ]
}