Skip to content

Commit

Permalink
Add API documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
loicknuchel committed Jan 20, 2025
1 parent 84d4564 commit 4d881a1
Show file tree
Hide file tree
Showing 12 changed files with 281 additions and 45 deletions.
5 changes: 3 additions & 2 deletions backend/lib/azimutt_web/controllers/api/source_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ defmodule AzimuttWeb.Api.SourceController do
{:ok, json} <- Jason.decode(content),
{:ok, source} <- json["sources"] |> Enum.find(fn s -> s["id"] == source_id end) |> Result.from_nillable(),
:ok <- if(source["kind"]["kind"] == "AmlEditor", do: {:error, {:forbidden, "AML sources can't be updated via API."}}, else: :ok),
json_updated = json |> Map.put("sources", json["sources"] |> Enum.map(fn s -> if(s["id"] == source_id, do: update_source(s, body), else: s) end)),
json_updated = json |> Map.put("sources", json["sources"] |> Enum.map(fn s -> if(s["id"] == source_id, do: update_source(s, body, now), else: s) end)),
{:ok, content_updated} <- Jason.encode(json_updated),
{:ok, %Project{} = _project_updated} <- Projects.update_project_file(project, content_updated, current_user, now),
do: conn |> render("show.json", source: source, ctx: ctx)
Expand Down Expand Up @@ -209,11 +209,12 @@ defmodule AzimuttWeb.Api.SourceController do
|> Map.put("updatedAt", DateTime.to_unix(now, :millisecond))
end

defp update_source(source, params) do
defp update_source(source, params, now) do
source
|> Map.put("tables", params["tables"])
|> Map.put("relations", params["relations"])
|> Mapx.put_no_nil("types", params["types"])
|> Map.put("updatedAt", DateTime.to_unix(now) * 1000)
end

def swagger_definitions do
Expand Down
4 changes: 4 additions & 0 deletions backend/lib/azimutt_web/templates/website/docs/_h4.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<h4 id={Azimutt.Utils.Slugme.slugify(@title)} class="group">
<%= @title %>
<a href={"##{Azimutt.Utils.Slugme.slugify(@title)}"} class="ml-1 text-indigo-600 no-underline hover:underline opacity-0 group-hover:opacity-100 transition-opacity">#</a>
</h4>
107 changes: 101 additions & 6 deletions backend/lib/azimutt_web/templates/website/docs/api.html.heex
Original file line number Diff line number Diff line change
@@ -1,17 +1,112 @@
<%= render "docs/_header.html", conn: @conn, page: @page %>

<%= doc_prose do %>
<p class="lead">Work In Progress 😅</p>
<p class="lead">
Azimutt is made for large and complex databases, which often require some kind of automation to be handled well.
It offers two kind of API with different purpose:
</p>
<ul>
<li><a href="#http-api">HTTP API</a>: sync Azimutt with the rest of your tools</li>
<li><a href="#javascript-api">JavaScript API</a>: script your actions within the editor</li>
</ul>

<%= render "docs/_h2.html", title: "HTTP API" %>
<p class="lead">Work In Progress 😅</p>
<p>See <a href="/api/v1/swagger" target="_blank" rel="noopener">Swagger spec</a>.</p>
<p class="lead">You can use the Azimutt HTTP API to import, export or update projects from code. For example to sync Azimutt with other sources you may have.</p>
<p>Check the whole <a href="/api/v1/swagger" target="_blank" rel="noopener">Swagger spec</a> for available endpoints, the two main parts are:</p>
<ul>
<li><a href="#source-management">Source management</a></li>
<li><a href="#schema-documentation">Schema documentation</a></li>
</ul>
<p>If you miss some endpoints, let us know and we will add them.</p>
<p>
The API authentication with made through a custom header token that will impersonate you.
You can create one from your <a href={"#{Routes.user_settings_path(@conn, :show)}#auth-tokens-social-heading"} target="_blank" rel="noopener">user settings</a> (see "Your authentication tokens" at the bottom, hover the gray box to see it).
Then put it on a <code>auth-token</code> request header, it will authenticate you through the API.
</p>
<p><img src={Routes.static_path(@conn, "/images/doc/api-user-token.png")} alt="Create your API token"></p>

<%= render "docs/_h3.html", title: "Source schema" %>
<p class="lead">Work In Progress 😅</p>
<%= render "docs/_h3.html", title: "Source management" %>
<p class="lead">The Source API let you fetch sources of a project and perform CRUD operations.</p>
<p>Here is how to use the API with TypeScript:</p>
<pre><code class="hljs js">function getSources(orgId: string, projectId: string, authToken: string): Promise&lt;SourceInfo[]> {
return fetch(
`https://azimutt.app/api/v1/organizations/${orgId}/projects/${projectId}/sources`,
{headers: {'auth-token': authToken}}
).then(res => res.json())
}
</code></pre>

<%= render "docs/_h4.html", title: "Automatically update your source" %>
<p>
The main usage for this API is to keep your source always up-to-date with your database.
For example, you can push your database schema to Azimutt at every change using a GitHub action and Azimutt libraries.
</p>
<p>
Here is a sample script for PostgreSQL using <a href="https://www.npmjs.com/package/@azimutt/connector-postgres" target="_blank" rel="noopener noreferrer">@azimutt/connector-postgres</a> and
<a href="https://www.npmjs.com/package/@azimutt/models" target="_blank" rel="noopener noreferrer">@azimutt/models</a>:
</p>
<pre><code class="hljs js">import {Database, databaseToSourceContent, LegacySource, LegacySourceContent, parseDatabaseUrl} from "@azimutt/models"
import {postgres} from "@azimutt/connector-postgres"

const azimuttApi = 'https://azimutt.app/api/v1'
const authToken = '2c7cb13d-d2b2-42f8-8eef-f447ab3591db' // from https://azimutt.app/settings
const organizationId = '1749e94e-158a-8419-936f-dc22fff4dac8' // from Azimutt url
const projectId = '2711536a-c539-4096-b685-d42bc93f93fe' // from Azimutt url
const sourceId = '072fd32c-e11d-8a14-a674-0842600f9ae0' // from API source list
const databaseUrl = 'postgresql://postgres:postgres@localhost/azimutt_dev' // you should have it ^^

const database: Database = await postgres.getSchema(
'source updater',
parseDatabaseUrl(databaseUrl),
{logger: {
debug: (text: string): void => console.debug(text),
log: (text: string): void => console.log(text),
warn: (text: string): void => console.warn(text),
error: (text: string): void => console.error(text),
}}
)
const source: LegacySourceContent = databaseToSourceContent(database)
await updateSource(organizationId, projectId, sourceId, source, authToken)

function updateSource(orgId: string, projectId: string, sourceId: string, content: LegacySourceContent, authToken: string): Promise&lt;LegacySource> {
return fetch(`${azimuttApi}/organizations/${orgId}/projects/${projectId}/sources/${sourceId}`, {
method: 'PUT',
headers: {'auth-token': authToken, 'Content-Type': 'application/json'},
body: JSON.stringify(content)
}).then(res => res.json())
}
</code></pre>

<%= render "docs/_h3.html", title: "Schema documentation" %>
<p class="lead">Work In Progress 😅</p>
<p class="lead">The documentation API let you get or set all the documentation from Azimutt.</p>
<p>
A good use case is to sync Azimutt with another documentation tool, having one or the other as a golden source (one way sync).
Here is the minimal code you may need:
</p>
<pre><code class="hljs js">type Metadata = {[tableId: string]: TableDoc & {columns: {[columnPath: string]: ColumnDoc}}}
type TableDoc = {notes?: string, tags?: string[], color?: string}
type ColumnDoc = {notes?: string, tags?: string[]}

function getDocumentation(orgId: string, projectId: string, authToken: string): Promise&lt;Metadata> {
return fetch(`${azimuttApi}/organizations/${orgId}/projects/${projectId}/metadata`, {
headers: {'auth-token': authToken}
}).then(res => res.json())
}

function putDocumentation(orgId: string, projectId: string, doc: Metadata, authToken: string) {
return fetch(`${azimuttApi}/organizations/${orgId}/projects/${projectId}/metadata`, {
method: 'PUT',
headers: {'auth-token': authToken, 'Content-Type': 'application/json'},
body: JSON.stringify(doc)
}).then(res => res.json())
}

const doc = await getDocumentation(orgId, projectId, authToken)
// doc['public.users'] = {notes: 'Some notes', tags: ['api'], color: 'blue', columns: {name: {notes: 'Some notes', tags: ['api']}}}
// delete doc['public.users']
await putDocumentation(orgId, projectId, doc, authToken)
</code></pre>
<p>These methods get and update the whole documentation, there is also endpoints for a specific table or column if you need more granular access.</p>

<%= render "docs/_h2.html", title: "JavaScript API" %>
<p class="lead">Work In Progress 😅</p>
Expand Down
8 changes: 6 additions & 2 deletions backend/lib/azimutt_web/utils/project_schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ defmodule AzimuttWeb.Utils.ProjectSchema do
"properties" => %{
"notes" => %{"type" => "string"},
"tags" => %{"type" => "array", "items" => %{"type" => "string"}},
"color" => %{"enum" => ["indigo", "violet", "purple", "fuchsia", "pink", "rose", "red", "orange", "amber", "yellow", "lime", "green", "emerald", "teal", "cyan", "sky", "blue"]},
"columns" => %{"type" => "object", "additionalProperties" => @column_meta}
}
}
Expand Down Expand Up @@ -128,7 +129,8 @@ defmodule AzimuttWeb.Utils.ProjectSchema do
"comment" => @comment,
"values" => %{"type" => "array", "items" => %{"type" => "string"}},
# MUST include the column inside the `definitions` attribute in the global schema
"columns" => %{"type" => "array", "items" => %{"$ref" => "#/definitions/column"}}
"columns" => %{"type" => "array", "items" => %{"$ref" => "#/definitions/column"}},
"stats" => %{"type" => "object"}
}
}

Expand All @@ -140,12 +142,14 @@ defmodule AzimuttWeb.Utils.ProjectSchema do
"schema" => %{"type" => "string"},
"table" => %{"type" => "string"},
"view" => %{"type" => "boolean"},
"definition" => %{"type" => "string"},
"columns" => %{"type" => "array", "items" => @column},
"primaryKey" => @primary_key,
"uniques" => %{"type" => "array", "items" => @unique},
"indexes" => %{"type" => "array", "items" => @index},
"checks" => %{"type" => "array", "items" => @check},
"comment" => @comment
"comment" => @comment,
"stats" => %{"type" => "object"}
}
}

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 25 additions & 14 deletions gateway/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@azimutt/connector-postgres": "^0.1.11",
"@azimutt/connector-snowflake": "^0.1.2",
"@azimutt/connector-sqlserver": "^0.1.4",
"@azimutt/models": "^0.1.17",
"@azimutt/models": "^0.1.19",
"@azimutt/utils": "^0.1.8",
"@fastify/cors": "9.0.1",
"@sinclair/typebox": "0.29.6",
Expand Down
2 changes: 1 addition & 1 deletion libs/models/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@azimutt/models",
"description": "Define a standard database models for Azimutt.",
"version": "0.1.18",
"version": "0.1.19",
"license": "MIT",
"homepage": "https://azimutt.app",
"keywords": [],
Expand Down
1 change: 1 addition & 0 deletions libs/models/src/databaseUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ export function attributeValueToString(value: AttributeValue): string {
if (typeof value === 'number') return value.toString()
if (typeof value === 'boolean') return value.toString()
if (value instanceof Date) return value.toISOString()
if (value === undefined) return 'null'
if (value === null) return 'null'
return JSON.stringify(value)
}
Expand Down
Loading

0 comments on commit 4d881a1

Please sign in to comment.