# NisRest REST API data bridge for NIS — converts JSON string-keyed params to Nis atom-keyed calls and returns maps ready for JSON encoding. --- ## Table of Contents 1. [Overview](#overview) 2. [Features](#features) 3. [Usage](#usage) 4. [API Reference](#api-reference) 5. [Param Parsing](#param-parsing) 6. [Related Documentation](#related-documentation) --- ## Overview `NisRest` is a pure functional module (no GenServer, no state) that acts as a translation layer between JSON-based REST/MCP inputs and the `Nis` functional module. Every function accepts a `username` and string-keyed params (as decoded from JSON), converts them to atom-keyed maps with proper types, and delegates to the corresponding `Nis` function. This module exists to keep type coercion and input validation out of both the router layer and the core `Nis` module. The router handles HTTP concerns (status codes, auth), `NisRest` handles data transformation, and `Nis` handles business logic. ## Features | Feature | Description | |---------|-------------| | Contacts CRUD | List, get, create, update, delete contacts | | Companies CRUD | List, get, create, update, delete companies | | Websites CRUD | List, get, create, update, delete websites | | Activities CRUD | List, get, create, update, delete, complete activities | | Notes CRUD | List, get, create, update, delete notes | | Search | Full-text search via FTS5 | | Due items | Overdue follow-ups across all entity types | | Follow-ups | Today/upcoming follow-ups | | Birthdays | Contacts with upcoming birthdays | | Pipeline | Sales stage grouping with contact/company split | | Log interaction | Create note + update contact's last_contact_at | | Stats | Activity metrics over a time range | | Bulk operations | Bulk stage updates, contact/company imports | ## Usage ### Via MCP Tools ```elixir # MCP server calls NisRest functions with the authenticated username NisRest.list_contacts("james", %{"stage" => "outreach", "priority_min" => "3"}) NisRest.create_contact("james", %{"name" => "Bob", "tags" => "client,vip"}) ``` ### Via REST Router ```elixir # Router extracts params from JSON body, passes to NisRest NisRest.search("james", %{"q" => "acme"}) NisRest.pipeline("james") ``` ## API Reference ### Contacts #### `list_contacts/2` List contacts with optional filters. **Parameters:** - `username` (string) - `params` (map, optional) — string-keyed: `"q"`, `"tag"`, `"sort"`, `"stage"`, `"priority_min"`, `"company_id"`, `"limit"`, `"last_contact_before"`, `"next_contact_before"` **Returns:** `{:ok, list}` ```elixir {:ok, contacts} = NisRest.list_contacts("james", %{"stage" => "outreach"}) ``` #### `get_contact/2` Fetch a contact by ID. **Returns:** `{:ok, map}` or `{:error, :not_found}` ```elixir {:ok, contact} = NisRest.get_contact("james", 42) ``` #### `create_contact/2` Create a contact from JSON params. **Params:** `"name"` (required), `"nickname"`, `"company_id"`, `"background"`, `"location"`, `"address"`, `"contact_medium"`, `"relation"`, `"birthday"`, `"priority"`, `"stage"`, `"contact_every"`, `"referred_by"`, `"interests"`, `"tags"` **Returns:** `{:ok, id}` ```elixir {:ok, id} = NisRest.create_contact("james", %{"name" => "Alice", "tags" => "friend,dev"}) ``` #### `update_contact/3` Update a contact. **Returns:** `{:ok, updated_at}` or `{:error, reason}` ```elixir {:ok, _} = NisRest.update_contact("james", 42, %{"stage" => "follow_up"}) ``` #### `delete_contact/2` Delete a contact with cascading cleanup. **Returns:** `{:ok, count}` ```elixir {:ok, 1} = NisRest.delete_contact("james", 42) ``` ### Companies #### `list_companies/2` **Params:** `"q"`, `"tag"`, `"sort"`, `"stage"`, `"priority_min"`, `"limit"`, `"last_contact_before"`, `"next_contact_before"` **Returns:** `{:ok, list}` ```elixir {:ok, companies} = NisRest.list_companies("james", %{"stage" => "demo"}) ``` #### `get_company/2` **Returns:** `{:ok, map}` or `{:error, :not_found}` ```elixir {:ok, company} = NisRest.get_company("james", 99) ``` #### `create_company/2` **Params:** `"name"` (required), `"location"`, `"purpose"`, `"interest"`, `"priority"`, `"stage"`, `"deal_value"`, `"expected_close_at"`, `"tags"` **Returns:** `{:ok, id}` ```elixir {:ok, id} = NisRest.create_company("james", %{"name" => "TechCo", "deal_value" => "50000"}) ``` #### `update_company/3` **Returns:** `{:ok, updated_at}` or `{:error, reason}` ```elixir {:ok, _} = NisRest.update_company("james", 99, %{"stage" => "closed_won"}) ``` #### `delete_company/2` **Returns:** `{:ok, count}` ```elixir {:ok, 1} = NisRest.delete_company("james", 99) ``` ### Websites #### `list_websites/2` **Params:** `"q"`, `"tag"`, `"sort"`, `"company_id"`, `"contact_id"`, `"limit"` **Returns:** `{:ok, list}` ```elixir {:ok, websites} = NisRest.list_websites("james", %{"company_id" => "99"}) ``` #### `get_website/2` **Returns:** `{:ok, map}` or `{:error, :not_found}` ```elixir {:ok, website} = NisRest.get_website("james", 123) ``` #### `create_website/2` **Params:** `"url"` (required), `"purpose"`, `"company_id"`, `"contact_id"`, `"tags"` **Returns:** `{:ok, id}` ```elixir {:ok, id} = NisRest.create_website("james", %{"url" => "https://example.com"}) ``` #### `update_website/3` **Returns:** `{:ok, updated_at}` or `{:error, reason}` ```elixir {:ok, _} = NisRest.update_website("james", 123, %{"purpose" => "Blog"}) ``` #### `delete_website/2` **Returns:** `{:ok, count}` ```elixir {:ok, 1} = NisRest.delete_website("james", 123) ``` ### Activities #### `list_activities/2` **Params:** `"q"`, `"tag"`, `"sort"`, `"status"`, `"entity_type"`, `"entity_id"`, `"limit"`, `"completed_after"`, `"completed_before"` **Returns:** `{:ok, list}` ```elixir {:ok, activities} = NisRest.list_activities("james", %{"status" => "open"}) ``` #### `get_activity/2` Returns the activity with its entity links. **Returns:** `{:ok, map}` or `{:error, :not_found}` ```elixir {:ok, activity} = NisRest.get_activity("james", 200) ``` #### `create_activity/2` **Params:** `"title"` (required), `"description"`, `"status"`, `"recurrence"`, `"tags"`, `"links"` (JSON array) **Returns:** `{:ok, id}` ```elixir {:ok, id} = NisRest.create_activity("james", %{"title" => "Weekly sync", "recurrence" => "weekly"}) ``` #### `update_activity/3` **Returns:** `{:ok, updated_at}` or `{:error, reason}` ```elixir {:ok, _} = NisRest.update_activity("james", 200, %{"status" => "done"}) ``` #### `delete_activity/2` **Returns:** `{:ok, count}` ```elixir {:ok, 1} = NisRest.delete_activity("james", 200) ``` #### `complete_activity/2` Complete an activity. Handles recurrence automatically. **Returns:** `{:ok, %{completed_id: id}}` or `{:ok, %{completed_id: id, next_id: id}}` ```elixir {:ok, result} = NisRest.complete_activity("james", 200) ``` ### Notes #### `list_notes/2` **Params:** `"entity_type"`, `"entity_id"`, `"tag"`, `"limit"`, `"since"` **Returns:** `{:ok, list}` ```elixir {:ok, notes} = NisRest.list_notes("james", %{"entity_type" => "contact", "entity_id" => "42"}) ``` #### `get_note/2` **Returns:** `{:ok, map}` or `{:error, :not_found}` ```elixir {:ok, note} = NisRest.get_note("james", 300) ``` #### `create_note/2` **Params:** `"entity_type"`, `"entity_id"`, `"content"`, `"reason"`, `"url"`, `"channel"` (JSON), `"tags"`, `"is_next_contact"`, `"next_contact_at"` **Returns:** `{:ok, id}` ```elixir {:ok, id} = NisRest.create_note("james", %{"entity_type" => "contact", "entity_id" => "42", "content" => "Called"}) ``` #### `update_note/3` **Returns:** `{:ok, updated_at}` or `{:error, reason}` ```elixir {:ok, _} = NisRest.update_note("james", 300, %{"content" => "Updated"}) ``` #### `delete_note/2` **Returns:** `{:ok, count}` ```elixir {:ok, 1} = NisRest.delete_note("james", 300) ``` ### Queries #### `search/2` Full-text search. Requires `"q"` param. **Returns:** `{:ok, list}` or `{:error, "q is required"}` ```elixir {:ok, results} = NisRest.search("james", %{"q" => "acme"}) ``` #### `due/1` Get overdue items across all entity types. **Returns:** map with `"contacts"`, `"companies"`, `"websites"`, `"activities"`, `"tasks"` ```elixir due_items = NisRest.due("james") ``` #### `followups/1` Get follow-ups grouped by overdue/today/upcoming. **Returns:** map with `"overdue"`, `"today"`, `"upcoming"`, `"counts"` ```elixir followups = NisRest.followups("james") ``` #### `birthdays/1` Get contacts with upcoming birthdays. **Returns:** list of contact maps ```elixir birthdays = NisRest.birthdays("james") ``` #### `pipeline/1` Get pipeline data split into contacts and companies with stage totals. Transforms the grouped Nis output into a flat format for the web UI. **Returns:** `%{contacts: [...], companies: [...], totals: %{"stage" => count}}` ```elixir %{contacts: cs, companies: cos, totals: t} = NisRest.pipeline("james") ``` #### `company_contacts/2` Get contacts belonging to a company. **Returns:** list of contact maps ```elixir contacts = NisRest.company_contacts("james", 99) ``` ### Convenience #### `log_interaction/2` Log an interaction with a contact. Requires `"contact_id"` and `"content"`. **Params:** `"contact_id"`, `"content"`, `"channel"` (JSON), `"tags"`, `"next_action"`, `"next_action_due"` **Returns:** `{:ok, note_id}` or `{:ok, %{note_id, next_note_id}}` or `{:error, reason}` ```elixir {:ok, result} = NisRest.log_interaction("james", %{ "contact_id" => "42", "content" => "Demo call", "channel" => "[\"video\"]" }) ``` #### `stats/2` Get CRM statistics for a time range. **Params:** `"since"` (Unix seconds, default 0), `"until"` (Unix seconds, default now) **Returns:** `{:ok, map}` ```elixir {:ok, stats} = NisRest.stats("james", %{"since" => "1708992000"}) ``` #### `bulk_stage_update/2` Move multiple entities to a pipeline stage. Requires `"stage"` and `"entities"`. **Returns:** `{:ok, results}` or `{:error, "stage is required"}` ```elixir {:ok, results} = NisRest.bulk_stage_update("james", %{ "stage" => "follow_up", "entities" => [%{"entity_type" => "contact", "entity_id" => 42}] }) ``` #### `import_contacts/2` Bulk import contacts from a list. **Params:** `"items"` — list of contact param maps **Returns:** `{:ok, %{created: [ids], errors: [%{index, error}]}}` ```elixir {:ok, result} = NisRest.import_contacts("james", %{ "items" => [%{"name" => "Alice"}, %{"name" => "Bob"}] }) ``` #### `import_companies/2` Bulk import companies from a list. **Returns:** `{:ok, %{created: [ids], errors: [%{index, error}]}}` ```elixir {:ok, result} = NisRest.import_companies("james", %{ "items" => [%{"name" => "TechCo"}, %{"name" => "DataInc"}] }) ``` ## Param Parsing `NisRest` handles type coercion from JSON strings to Elixir types: | Helper | Behavior | |--------|----------| | `put_string` | Trims whitespace, drops nil/empty | | `put_integer` | Parses string integers, truncates floats | | `put_time` | Accepts Unix integers, ISO 8601 strings, or `0` (→ nil) | | `put_json_tags` | Splits comma-separated strings into list, passes lists through | | `put_json_passthrough` | Decodes JSON strings, passes lists/maps through | Time parsing supports both Unix timestamps (`"1709164800"`) and ISO 8601 (`"2024-02-28T14:00"`). A value of `0` clears the time field to nil. ## Related Documentation - [Nis](nis.md) — core CRM module - [NisTask](nis_task.md) — task management - [NisTaskRest](nis_task_rest.md) — REST bridge for tasks - [NIS User Guide](nis-user-guide.md) — end-user guide --- *Source: `lib/nis_rest.ex` — Last updated: 2026-02-27*