{
  "schemaVersion": 1,
  "scope": {
    "prefix": "x_eq_demo",
    "name": "Event Queue example",
    "create": true
  },
  "updateSet": "Event Queue example",
  "steps": [
    {
      "id": "contract",
      "skill": "table",
      "op": "create_table",
      "inputs": {
        "name": "x_eq_demo_contract",
        "label": "Event Queue contract",
        "sysScope": "$scope",
        "schema": [
          {
            "column": "type",
            "label": "type",
            "internalType": "string"
          },
          {
            "column": "threshold",
            "label": "threshold",
            "internalType": "integer"
          },
          {
            "column": "active",
            "label": "active",
            "internalType": "boolean"
          }
        ]
      }
    },
    {
      "id": "event",
      "skill": "table",
      "op": "create_table",
      "inputs": {
        "name": "x_eq_demo_event",
        "label": "Event Queue event",
        "sysScope": "$scope",
        "schema": [
          {
            "column": "event_id",
            "label": "event_id",
            "internalType": "string"
          },
          {
            "column": "payload",
            "label": "payload",
            "internalType": "string",
            "maxLength": 32768
          },
          {
            "column": "state",
            "label": "state",
            "internalType": "string"
          },
          {
            "column": "route",
            "label": "route",
            "internalType": "string"
          },
          {
            "column": "contract_ref",
            "label": "contract_ref",
            "internalType": "reference",
            "reference": "x_eq_demo_contract"
          },
          {
            "column": "delivery",
            "label": "delivery",
            "internalType": "string",
            "maxLength": 32
          }
        ]
      }
    },
    {
      "id": "delivery",
      "skill": "table",
      "op": "create_table",
      "inputs": {
        "name": "x_eq_demo_delivery",
        "label": "Event Queue delivery",
        "sysScope": "$scope",
        "schema": [
          {
            "column": "event",
            "label": "event",
            "internalType": "reference",
            "reference": "x_eq_demo_event"
          },
          {
            "column": "order_id",
            "label": "order_id",
            "internalType": "string"
          },
          {
            "column": "amount",
            "label": "amount",
            "internalType": "integer"
          },
          {
            "column": "currency",
            "label": "currency",
            "internalType": "string"
          },
          {
            "column": "route",
            "label": "route",
            "internalType": "string"
          }
        ]
      }
    },
    {
      "id": "api",
      "skill": "scripted-rest",
      "op": "create_api",
      "inputs": {
        "name": "Event Queue",
        "serviceId": "event_queue",
        "sysScope": "$scope",
        "consumes": "application/json",
        "produces": "application/json"
      }
    },
    {
      "id": "submit",
      "skill": "scripted-rest",
      "op": "create_resource",
      "inputs": {
        "name": "submit",
        "method": "POST",
        "path": "/events",
        "script": "(function (request, response) {\n  function reply(status, body) { response.setStatus(status); response.setBody(body); }\n  function fail(status, text) { reply(status, { error: { message: text } }); }\n  function find(table, field, val) {\n    var gr = new GlideRecord(table); gr.addQuery(field, val); gr.query(); return gr;\n  }\n  function contract() {\n    var c = new GlideRecord('x_eq_demo_contract');\n    c.addQuery('type', 'order.created'); c.addQuery('active', true); c.query();\n    if (!c.next()) throw new Error('Missing active contract');\n    var result = { id: c.getUniqueValue(), threshold: Number(c.getValue('threshold')) };\n    if (!isFinite(result.threshold) || c.next()) throw new Error('Invalid or ambiguous contract');\n    return result;\n  }\n  function canonical(v) {\n    if (v === null || typeof v !== 'object') return JSON.stringify(v);\n    var keys = Object.keys(v).sort(), parts = [];\n    for (var i = 0; i < keys.length; i++) parts.push(JSON.stringify(keys[i]) + ':' + canonical(v[keys[i]]));\n    return '{' + parts.join(',') + '}';\n  }\n  function keysAllowed(obj, allowed) {\n    var keys = Object.keys(obj);\n    for (var i = 0; i < keys.length; i++) if (allowed.indexOf(keys[i]) < 0) return false;\n    return true;\n  }\n  try {\n    var mode = 'submit', eventId, gr, payload, c;\n    if (mode === 'submit') {\n      var body;\n      try { body = JSON.parse(String(request.body.dataString)); }\n      catch (parseError) { fail(400, 'Malformed JSON'); return; }\n      if (!body || typeof body !== 'object' || Array.isArray(body)) { fail(400, 'Object required'); return; }\n      payload = body.payload; eventId = body.event_id;\n      var identifier = /^[A-Za-z0-9_-]{1,64}$/;\n      if (!keysAllowed(body, ['event_id', 'event_type', 'payload']) ||\n          typeof eventId !== 'string' || !identifier.test(eventId) || body.event_type !== 'order.created' ||\n          !payload || typeof payload !== 'object' || Array.isArray(payload) ||\n          !keysAllowed(payload, ['order_id', 'amount', 'currency', 'note']) ||\n          typeof payload.order_id !== 'string' || !identifier.test(payload.order_id) ||\n          typeof payload.amount !== 'number' || !isFinite(payload.amount) ||\n          Math.floor(payload.amount) !== payload.amount || payload.amount < 0 || payload.amount > 1000000 ||\n          (payload.currency !== 'USD' && payload.currency !== 'EUR') ||\n          (Object.prototype.hasOwnProperty.call(payload, 'note') && typeof payload.note !== 'string')) {\n        fail(422, 'Invalid event'); return;\n      }\n      if (payload.note !== undefined) {\n        var bytes;\n        try { bytes = encodeURIComponent(payload.note).replace(/%[0-9A-F]{2}/gi, 'x').length; }\n        catch (unicodeError) { fail(422, 'Invalid Unicode'); return; }\n        if (bytes > 4096) { fail(413, 'Note exceeds 4096 UTF-8 bytes'); return; }\n      }\n      gr = find('x_eq_demo_event', 'event_id', eventId);\n      if (gr.next()) {\n        if (canonical(JSON.parse(gr.getValue('payload'))) !== canonical(payload)) { fail(409, 'Conflicting duplicate'); return; }\n        reply(200, { event_id: eventId, record_id: gr.getUniqueValue(), state: gr.getValue('state'), duplicate: true }); return;\n      }\n      c = contract();\n      gr.initialize(); gr.setValue('event_id', eventId); gr.setValue('payload', JSON.stringify(payload));\n      gr.setValue('state', 'queued'); gr.setValue('contract_ref', c.id);\n      var eventRecord = gr.insert();\n      if (!eventRecord) throw new Error('Event insert failed');\n      reply(202, { event_id: eventId, record_id: String(eventRecord), state: 'queued', duplicate: false }); return;\n    }\n    eventId = request.pathParams.event_id;\n    gr = find('x_eq_demo_event', 'event_id', eventId);\n    if (!gr.next()) { fail(404, 'Unknown event'); return; }\n    payload = JSON.parse(gr.getValue('payload'));\n    if (mode === 'process' && gr.getValue('state') !== 'processed') {\n      c = contract();\n      var delivery = new GlideRecord('x_eq_demo_delivery'); delivery.initialize();\n      delivery.setValue('event', gr.getUniqueValue()); delivery.setValue('order_id', payload.order_id);\n      delivery.setValue('amount', payload.amount); delivery.setValue('currency', payload.currency);\n      var route = payload.amount < c.threshold ? 'standard' : 'priority'; delivery.setValue('route', route);\n      var deliveryId = delivery.insert();\n      if (!deliveryId) throw new Error('Delivery insert failed');\n      gr.setValue('state', 'processed'); gr.setValue('route', route); gr.setValue('delivery', deliveryId);\n      if (!gr.update()) throw new Error('Event update failed');\n    }\n    var result = { event_id: eventId, state: gr.getValue('state'),\n      route: gr.getValue('route') || null, delivery_id: gr.getValue('delivery') || null };\n    if (mode === 'read') {\n      result.record_id = gr.getUniqueValue(); result.event_type = 'order.created'; result.payload = payload;\n    }\n    reply(200, result);\n  } catch (error) { fail(500, String(error.message || error)); }\n})(request, response);\n",
        "webServiceDefinition": "$api",
        "sysScope": "$scope",
        "requiresAuthentication": true,
        "requiresAclAuthorization": false
      }
    },
    {
      "id": "process",
      "skill": "scripted-rest",
      "op": "create_resource",
      "inputs": {
        "name": "process",
        "method": "POST",
        "path": "/process/{event_id}",
        "script": "(function (request, response) {\n  function reply(status, body) { response.setStatus(status); response.setBody(body); }\n  function fail(status, text) { reply(status, { error: { message: text } }); }\n  function find(table, field, val) {\n    var gr = new GlideRecord(table); gr.addQuery(field, val); gr.query(); return gr;\n  }\n  function contract() {\n    var c = new GlideRecord('x_eq_demo_contract');\n    c.addQuery('type', 'order.created'); c.addQuery('active', true); c.query();\n    if (!c.next()) throw new Error('Missing active contract');\n    var result = { id: c.getUniqueValue(), threshold: Number(c.getValue('threshold')) };\n    if (!isFinite(result.threshold) || c.next()) throw new Error('Invalid or ambiguous contract');\n    return result;\n  }\n  function canonical(v) {\n    if (v === null || typeof v !== 'object') return JSON.stringify(v);\n    var keys = Object.keys(v).sort(), parts = [];\n    for (var i = 0; i < keys.length; i++) parts.push(JSON.stringify(keys[i]) + ':' + canonical(v[keys[i]]));\n    return '{' + parts.join(',') + '}';\n  }\n  function keysAllowed(obj, allowed) {\n    var keys = Object.keys(obj);\n    for (var i = 0; i < keys.length; i++) if (allowed.indexOf(keys[i]) < 0) return false;\n    return true;\n  }\n  try {\n    var mode = 'process', eventId, gr, payload, c;\n    if (mode === 'submit') {\n      var body;\n      try { body = JSON.parse(String(request.body.dataString)); }\n      catch (parseError) { fail(400, 'Malformed JSON'); return; }\n      if (!body || typeof body !== 'object' || Array.isArray(body)) { fail(400, 'Object required'); return; }\n      payload = body.payload; eventId = body.event_id;\n      var identifier = /^[A-Za-z0-9_-]{1,64}$/;\n      if (!keysAllowed(body, ['event_id', 'event_type', 'payload']) ||\n          typeof eventId !== 'string' || !identifier.test(eventId) || body.event_type !== 'order.created' ||\n          !payload || typeof payload !== 'object' || Array.isArray(payload) ||\n          !keysAllowed(payload, ['order_id', 'amount', 'currency', 'note']) ||\n          typeof payload.order_id !== 'string' || !identifier.test(payload.order_id) ||\n          typeof payload.amount !== 'number' || !isFinite(payload.amount) ||\n          Math.floor(payload.amount) !== payload.amount || payload.amount < 0 || payload.amount > 1000000 ||\n          (payload.currency !== 'USD' && payload.currency !== 'EUR') ||\n          (Object.prototype.hasOwnProperty.call(payload, 'note') && typeof payload.note !== 'string')) {\n        fail(422, 'Invalid event'); return;\n      }\n      if (payload.note !== undefined) {\n        var bytes;\n        try { bytes = encodeURIComponent(payload.note).replace(/%[0-9A-F]{2}/gi, 'x').length; }\n        catch (unicodeError) { fail(422, 'Invalid Unicode'); return; }\n        if (bytes > 4096) { fail(413, 'Note exceeds 4096 UTF-8 bytes'); return; }\n      }\n      gr = find('x_eq_demo_event', 'event_id', eventId);\n      if (gr.next()) {\n        if (canonical(JSON.parse(gr.getValue('payload'))) !== canonical(payload)) { fail(409, 'Conflicting duplicate'); return; }\n        reply(200, { event_id: eventId, record_id: gr.getUniqueValue(), state: gr.getValue('state'), duplicate: true }); return;\n      }\n      c = contract();\n      gr.initialize(); gr.setValue('event_id', eventId); gr.setValue('payload', JSON.stringify(payload));\n      gr.setValue('state', 'queued'); gr.setValue('contract_ref', c.id);\n      var eventRecord = gr.insert();\n      if (!eventRecord) throw new Error('Event insert failed');\n      reply(202, { event_id: eventId, record_id: String(eventRecord), state: 'queued', duplicate: false }); return;\n    }\n    eventId = request.pathParams.event_id;\n    gr = find('x_eq_demo_event', 'event_id', eventId);\n    if (!gr.next()) { fail(404, 'Unknown event'); return; }\n    payload = JSON.parse(gr.getValue('payload'));\n    if (mode === 'process' && gr.getValue('state') !== 'processed') {\n      c = contract();\n      var delivery = new GlideRecord('x_eq_demo_delivery'); delivery.initialize();\n      delivery.setValue('event', gr.getUniqueValue()); delivery.setValue('order_id', payload.order_id);\n      delivery.setValue('amount', payload.amount); delivery.setValue('currency', payload.currency);\n      var route = payload.amount < c.threshold ? 'standard' : 'priority'; delivery.setValue('route', route);\n      var deliveryId = delivery.insert();\n      if (!deliveryId) throw new Error('Delivery insert failed');\n      gr.setValue('state', 'processed'); gr.setValue('route', route); gr.setValue('delivery', deliveryId);\n      if (!gr.update()) throw new Error('Event update failed');\n    }\n    var result = { event_id: eventId, state: gr.getValue('state'),\n      route: gr.getValue('route') || null, delivery_id: gr.getValue('delivery') || null };\n    if (mode === 'read') {\n      result.record_id = gr.getUniqueValue(); result.event_type = 'order.created'; result.payload = payload;\n    }\n    reply(200, result);\n  } catch (error) { fail(500, String(error.message || error)); }\n})(request, response);\n",
        "webServiceDefinition": "$api",
        "sysScope": "$scope",
        "requiresAuthentication": true,
        "requiresAclAuthorization": false
      }
    },
    {
      "id": "read",
      "skill": "scripted-rest",
      "op": "create_resource",
      "inputs": {
        "name": "read",
        "method": "GET",
        "path": "/events/{event_id}",
        "script": "(function (request, response) {\n  function reply(status, body) { response.setStatus(status); response.setBody(body); }\n  function fail(status, text) { reply(status, { error: { message: text } }); }\n  function find(table, field, val) {\n    var gr = new GlideRecord(table); gr.addQuery(field, val); gr.query(); return gr;\n  }\n  function contract() {\n    var c = new GlideRecord('x_eq_demo_contract');\n    c.addQuery('type', 'order.created'); c.addQuery('active', true); c.query();\n    if (!c.next()) throw new Error('Missing active contract');\n    var result = { id: c.getUniqueValue(), threshold: Number(c.getValue('threshold')) };\n    if (!isFinite(result.threshold) || c.next()) throw new Error('Invalid or ambiguous contract');\n    return result;\n  }\n  function canonical(v) {\n    if (v === null || typeof v !== 'object') return JSON.stringify(v);\n    var keys = Object.keys(v).sort(), parts = [];\n    for (var i = 0; i < keys.length; i++) parts.push(JSON.stringify(keys[i]) + ':' + canonical(v[keys[i]]));\n    return '{' + parts.join(',') + '}';\n  }\n  function keysAllowed(obj, allowed) {\n    var keys = Object.keys(obj);\n    for (var i = 0; i < keys.length; i++) if (allowed.indexOf(keys[i]) < 0) return false;\n    return true;\n  }\n  try {\n    var mode = 'read', eventId, gr, payload, c;\n    if (mode === 'submit') {\n      var body;\n      try { body = JSON.parse(String(request.body.dataString)); }\n      catch (parseError) { fail(400, 'Malformed JSON'); return; }\n      if (!body || typeof body !== 'object' || Array.isArray(body)) { fail(400, 'Object required'); return; }\n      payload = body.payload; eventId = body.event_id;\n      var identifier = /^[A-Za-z0-9_-]{1,64}$/;\n      if (!keysAllowed(body, ['event_id', 'event_type', 'payload']) ||\n          typeof eventId !== 'string' || !identifier.test(eventId) || body.event_type !== 'order.created' ||\n          !payload || typeof payload !== 'object' || Array.isArray(payload) ||\n          !keysAllowed(payload, ['order_id', 'amount', 'currency', 'note']) ||\n          typeof payload.order_id !== 'string' || !identifier.test(payload.order_id) ||\n          typeof payload.amount !== 'number' || !isFinite(payload.amount) ||\n          Math.floor(payload.amount) !== payload.amount || payload.amount < 0 || payload.amount > 1000000 ||\n          (payload.currency !== 'USD' && payload.currency !== 'EUR') ||\n          (Object.prototype.hasOwnProperty.call(payload, 'note') && typeof payload.note !== 'string')) {\n        fail(422, 'Invalid event'); return;\n      }\n      if (payload.note !== undefined) {\n        var bytes;\n        try { bytes = encodeURIComponent(payload.note).replace(/%[0-9A-F]{2}/gi, 'x').length; }\n        catch (unicodeError) { fail(422, 'Invalid Unicode'); return; }\n        if (bytes > 4096) { fail(413, 'Note exceeds 4096 UTF-8 bytes'); return; }\n      }\n      gr = find('x_eq_demo_event', 'event_id', eventId);\n      if (gr.next()) {\n        if (canonical(JSON.parse(gr.getValue('payload'))) !== canonical(payload)) { fail(409, 'Conflicting duplicate'); return; }\n        reply(200, { event_id: eventId, record_id: gr.getUniqueValue(), state: gr.getValue('state'), duplicate: true }); return;\n      }\n      c = contract();\n      gr.initialize(); gr.setValue('event_id', eventId); gr.setValue('payload', JSON.stringify(payload));\n      gr.setValue('state', 'queued'); gr.setValue('contract_ref', c.id);\n      var eventRecord = gr.insert();\n      if (!eventRecord) throw new Error('Event insert failed');\n      reply(202, { event_id: eventId, record_id: String(eventRecord), state: 'queued', duplicate: false }); return;\n    }\n    eventId = request.pathParams.event_id;\n    gr = find('x_eq_demo_event', 'event_id', eventId);\n    if (!gr.next()) { fail(404, 'Unknown event'); return; }\n    payload = JSON.parse(gr.getValue('payload'));\n    if (mode === 'process' && gr.getValue('state') !== 'processed') {\n      c = contract();\n      var delivery = new GlideRecord('x_eq_demo_delivery'); delivery.initialize();\n      delivery.setValue('event', gr.getUniqueValue()); delivery.setValue('order_id', payload.order_id);\n      delivery.setValue('amount', payload.amount); delivery.setValue('currency', payload.currency);\n      var route = payload.amount < c.threshold ? 'standard' : 'priority'; delivery.setValue('route', route);\n      var deliveryId = delivery.insert();\n      if (!deliveryId) throw new Error('Delivery insert failed');\n      gr.setValue('state', 'processed'); gr.setValue('route', route); gr.setValue('delivery', deliveryId);\n      if (!gr.update()) throw new Error('Event update failed');\n    }\n    var result = { event_id: eventId, state: gr.getValue('state'),\n      route: gr.getValue('route') || null, delivery_id: gr.getValue('delivery') || null };\n    if (mode === 'read') {\n      result.record_id = gr.getUniqueValue(); result.event_type = 'order.created'; result.payload = payload;\n    }\n    reply(200, result);\n  } catch (error) { fail(500, String(error.message || error)); }\n})(request, response);\n",
        "webServiceDefinition": "$api",
        "sysScope": "$scope",
        "requiresAuthentication": true,
        "requiresAclAuthorization": false
      }
    }
  ],
  "test": {
    "structural": true
  }
}
