QlikElixir

View Source

Hex.pm Docs License

A comprehensive Elixir client for Qlik Cloud REST APIs and QIX Engine.

Features

REST APIs - Full coverage of Qlik Cloud management APIs:

  • Apps - Create, manage, publish, and export Qlik Sense applications
  • Spaces - Manage shared and managed spaces with role assignments
  • Data Files - Upload, manage, and organize data files
  • Reloads - Trigger and monitor app data reloads
  • Users & Groups - User management and access control
  • Automations - Create and run no-code workflows
  • Webhooks - Configure event notifications
  • And more - API Keys, Data Connections, Items, Collections, Reports, Report Templates, Natural Language

QIX Engine - Real-time data extraction via WebSocket:

  • Connect to apps and navigate sheets/objects
  • Extract hypercube data from visualizations (the core value!)
  • Make selections and filter data
  • Evaluate custom expressions
  • Stream large datasets efficiently

Installation

Add qlik_elixir to your dependencies in mix.exs:

def deps do
  [
    {:qlik_elixir, "~> 0.4.0"}
  ]
end

Quick Start

Configuration

# Option 1: Environment variables (recommended for production)
# QLIK_API_KEY=your-api-key
# QLIK_TENANT_URL=https://your-tenant.region.qlikcloud.com

# Option 2: Application config
config :qlik_elixir,
  api_key: "your-api-key",
  tenant_url: "https://your-tenant.region.qlikcloud.com"

# Option 3: Runtime config (for multiple tenants)
config = QlikElixir.new_config(
  api_key: "your-api-key",
  tenant_url: "https://your-tenant.region.qlikcloud.com"
)

REST API Examples

# List apps
{:ok, %{"data" => apps}} = QlikElixir.REST.Apps.list()

# Get app details
{:ok, app} = QlikElixir.REST.Apps.get("app-id")

# Trigger a reload
{:ok, reload} = QlikElixir.REST.Reloads.create("app-id")

# Upload a CSV file
{:ok, file} = QlikElixir.REST.DataFiles.upload_file("sales_data.csv")

# List spaces
{:ok, %{"data" => spaces}} = QlikElixir.REST.Spaces.list()

QIX Engine - Data Extraction

alias QlikElixir.QIX.{Session, App}

# Connect to an app
{:ok, session} = Session.connect("app-id", config: config)

# List sheets
{:ok, sheets} = App.list_sheets(session)

# Get visualization data (the main event!)
{:ok, data} = App.get_hypercube_data(session, "object-id")
# Returns:
# %{
#   headers: ["Country", "Sales", "Margin"],
#   rows: [
#     %{text: ["USA", "$1.2M", "23%"], values: ["USA", 1200000, 0.23]},
#     %{text: ["Germany", "$900K", "19%"], values: ["Germany", 900000, 0.19]}
#   ],
#   total_rows: 50,
#   truncated: false
# }

# Make selections
:ok = App.select_values(session, "Country", ["USA", "Germany"])

# Evaluate expressions
{:ok, total} = App.evaluate(session, "Sum(Sales)")

# Disconnect
:ok = Session.disconnect(session)

Reporting - PDF and Excel from an app

The Reporting API is asynchronous: you queue a request, poll it, then collect the outputs. generate/2 does all three.

alias QlikElixir.REST.Reports

request = %{
  type: "sense-sheet-1.0",
  senseSheetTemplate: %{appId: "app-id", sheet: %{id: "sheet-id"}},
  output: %{
    type: "pdf",
    outputId: "monthly",
    pdfOutput: %{size: "A4", orientation: "L", resizeType: "autofit"}
  },
  meta: %{outputTtl: "PT1H"}
}

{:ok, [output]} = Reports.generate(request, config: config)
{:ok, pdf} = Reports.download_output(output, config: config)
File.write!("monthly.pdf", pdf)

Queue now and collect later with create/2, get_status/2 and list_outputs/2.

API Reference

REST APIs

ModuleDescriptionQlik API Reference
QlikElixir.REST.AppsApp management, publishing, export/importApps API
QlikElixir.REST.SpacesSpaces and role assignmentsSpaces API
QlikElixir.REST.DataFilesFile upload and managementData Files API
QlikElixir.REST.ReloadsTrigger and monitor reloadsReloads API
QlikElixir.REST.UsersUser managementUsers API
QlikElixir.REST.GroupsGroup managementGroups API
QlikElixir.REST.APIKeysAPI key managementAPI Keys API
QlikElixir.REST.AutomationsAutomation workflowsAutomations API
QlikElixir.REST.WebhooksEvent notificationsWebhooks API
QlikElixir.REST.DataConnectionsExternal data sourcesData Connections API
QlikElixir.REST.ItemsUnified resource listingItems API
QlikElixir.REST.CollectionsContent organizationCollections API
QlikElixir.REST.ReportsAsynchronous report generationReporting API
QlikElixir.REST.ReportTemplatesStored report templatesReport Templates API
QlikElixir.REST.TenantsTenant configurationTenants API
QlikElixir.REST.RolesRole definitionsRoles API
QlikElixir.REST.AuditsAudit loggingAudits API
QlikElixir.REST.NaturalLanguageConversational analyticsInsight Advisor API

QIX Engine (WebSocket)

ModuleDescriptionQlik API Reference
QlikElixir.QIX.SessionWebSocket connection managementQIX Overview
QlikElixir.QIX.AppHigh-level data extraction APIDoc API
QlikElixir.QIX.ProtocolJSON-RPC protocol handlingGenericObject API

Core Modules

ModuleDescription
QlikElixir.ConfigConfiguration management
QlikElixir.ErrorError types and handling
QlikElixir.PaginationCursor-based pagination helpers

API Testing Status

All 397 tests and 22 doctests pass (Bypass HTTP mocking).

The following table shows integration testing status against real Qlik Cloud APIs:

ModuleReadWriteNotes
Appscreate, get, update, copy, delete, get_metadata, get_lineage, get_script, validate_script, list_media, get_thumbnail, export
Spacescreate, get, update, delete, list_types, list_assignments
DataFileslist, get, upload, delete, find_by_name
Reloadslist, get, create, cancel
Collectionscreate, get, update, delete, list_items, add_item, remove_item, get_favorites
Items-list, get, find_by_resource, get_published_items, get_collections
Users-me, list, count
Groups-list, list_settings
Roles-list, get
APIKeys-get_config requires tenant_id
Automations-list, list_runs
Webhooks-list, list_event_types
DataConnections-list, get
NaturalLanguage-get_model, list_analysis_types, ask, recommend
Audits-list, get, list_sources, list_types
Tenants-me
Reportscreate, get_status, list_outputs, await, generate, download_output
ReportTemplates-list

Untested Write Operations

The following write operations have unit tests but have not been integration tested:

ModuleUntested OperationsReason
Appspublish, import_appRequires published app setup
Spacescreate_assignment, delete_assignmentRequires user IDs
DataFilesupdate, changeowner, change_space, batch*Requires specific setup
Itemsupdate, deleteAffects catalog items
Userscreate, update, delete, inviteTenant admin operations
Groupscreate, update, delete, update_settingsGroup management
APIKeyscreate, update, delete, update_configSecurity sensitive
Automationscreate, update, delete, run, enable, disable, etc.Complex setup
Webhookscreate, update, delete, resend_deliveryRequires callback URL
ReportTemplatesget, create, update, patch, delete, downloadTenant has no stored templates to act on
DataConnectionscreate, update, deleteRequires datasourceID
Tenantsget, create, update, deactivate, reactivateTenant admin only

QIX Engine (WebSocket): ✅ Fully integration tested - Session, App, data extraction

Common Patterns

Pagination

All list endpoints support cursor-based pagination:

# First page
{:ok, %{"data" => apps, "links" => %{"next" => %{"href" => next_url}}}} =
  QlikElixir.REST.Apps.list(limit: 20)

# Get cursor from next URL and fetch next page
{:ok, page2} = QlikElixir.REST.Apps.list(limit: 20, next: cursor)

# Or use the Pagination helper
QlikElixir.Pagination.stream(fn cursor ->
  QlikElixir.REST.Apps.list(limit: 100, next: cursor)
end)
|> Enum.take(500)  # Get up to 500 apps

Error Handling

case QlikElixir.REST.Apps.get("app-id") do
  {:ok, app} ->
    IO.puts("Found app: #{app["name"]}")

  {:error, %QlikElixir.Error{type: :not_found}} ->
    IO.puts("App not found")

  {:error, %QlikElixir.Error{type: :authentication_error}} ->
    IO.puts("Invalid API key")

  {:error, %QlikElixir.Error{} = error} ->
    IO.puts("Error: #{error.message}")
end

Multiple Tenants

# Create configs for different tenants
us_config = QlikElixir.new_config(
  api_key: System.fetch_env!("US_QLIK_API_KEY"),
  tenant_url: "https://us-tenant.us.qlikcloud.com"
)

eu_config = QlikElixir.new_config(
  api_key: System.fetch_env!("EU_QLIK_API_KEY"),
  tenant_url: "https://eu-tenant.eu.qlikcloud.com"
)

# Use specific config per request
{:ok, us_apps} = QlikElixir.REST.Apps.list(config: us_config)
{:ok, eu_apps} = QlikElixir.REST.Apps.list(config: eu_config)

Streaming Large Datasets

alias QlikElixir.QIX.{Session, App}

{:ok, session} = Session.connect("app-id", config: config)

# Stream hypercube data in pages
App.stream_hypercube_data(session, "object-id", page_size: 1000)
|> Stream.flat_map(& &1)
|> Stream.each(fn row ->
  # Process each row
  IO.inspect(row)
end)
|> Stream.run()

Configuration Options

config = QlikElixir.new_config(
  # Required
  api_key: "your-api-key",
  tenant_url: "https://your-tenant.region.qlikcloud.com",

  # Optional
  connection_id: "default-connection-id",  # For data files
  http_options: [
    timeout: :timer.minutes(5),    # Request timeout
    retry: :transient,              # Retry strategy
    max_retries: 3,                 # Max retry attempts
    retry_delay: fn n -> n * 1000 end  # Backoff function
  ]
)

Development

# Install dependencies
mix deps.get

# Run tests
mix test

# Run with coverage
mix test --cover

# Check code quality
mix format --check-formatted
mix credo --strict
mix dialyzer

# Generate docs
mix docs

Roadmap / TODO

PRs welcome! Here are some areas that could use contribution:

  • [ ] Themes API - Manage app themes
  • [ ] Extensions API - Visualization extensions management
  • [ ] Brands API - Tenant branding configuration
  • [ ] Integration tests - More write operation coverage (see Untested Write Operations above)

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Write tests first (TDD encouraged)
  4. Ensure all checks pass (mix format && mix credo --strict && mix test)
  5. Commit your changes
  6. Push to the branch
  7. Open a Pull Request

License

MIT License - see LICENSE for details.


This project is proudly sponsored by Balneario - Clínica de Longevidad de Cofrentes, a world-class longevity clinic and thermal spa in Valencia, Spain. Their support makes open source development like this possible.

Thank you for investing in the developer community!