Skip to content

Commit 4d881a1

Browse files
committed
Add API documentation
1 parent 84d4564 commit 4d881a1

File tree

12 files changed

+281
-45
lines changed

12 files changed

+281
-45
lines changed

backend/lib/azimutt_web/controllers/api/source_controller.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ defmodule AzimuttWeb.Api.SourceController do
166166
{:ok, json} <- Jason.decode(content),
167167
{:ok, source} <- json["sources"] |> Enum.find(fn s -> s["id"] == source_id end) |> Result.from_nillable(),
168168
:ok <- if(source["kind"]["kind"] == "AmlEditor", do: {:error, {:forbidden, "AML sources can't be updated via API."}}, else: :ok),
169-
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)),
169+
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)),
170170
{:ok, content_updated} <- Jason.encode(json_updated),
171171
{:ok, %Project{} = _project_updated} <- Projects.update_project_file(project, content_updated, current_user, now),
172172
do: conn |> render("show.json", source: source, ctx: ctx)
@@ -209,11 +209,12 @@ defmodule AzimuttWeb.Api.SourceController do
209209
|> Map.put("updatedAt", DateTime.to_unix(now, :millisecond))
210210
end
211211

212-
defp update_source(source, params) do
212+
defp update_source(source, params, now) do
213213
source
214214
|> Map.put("tables", params["tables"])
215215
|> Map.put("relations", params["relations"])
216216
|> Mapx.put_no_nil("types", params["types"])
217+
|> Map.put("updatedAt", DateTime.to_unix(now) * 1000)
217218
end
218219

219220
def swagger_definitions do
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<h4 id={Azimutt.Utils.Slugme.slugify(@title)} class="group">
2+
<%= @title %>
3+
<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>
4+
</h4>

backend/lib/azimutt_web/templates/website/docs/api.html.heex

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,112 @@
11
<%= render "docs/_header.html", conn: @conn, page: @page %>
22

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

613
<%= render "docs/_h2.html", title: "HTTP API" %>
7-
<p class="lead">Work In Progress 😅</p>
8-
<p>See <a href="/api/v1/swagger" target="_blank" rel="noopener">Swagger spec</a>.</p>
14+
<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>
15+
<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>
16+
<ul>
17+
<li><a href="#source-management">Source management</a></li>
18+
<li><a href="#schema-documentation">Schema documentation</a></li>
19+
</ul>
20+
<p>If you miss some endpoints, let us know and we will add them.</p>
21+
<p>
22+
The API authentication with made through a custom header token that will impersonate you.
23+
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).
24+
Then put it on a <code>auth-token</code> request header, it will authenticate you through the API.
25+
</p>
26+
<p><img src={Routes.static_path(@conn, "/images/doc/api-user-token.png")} alt="Create your API token"></p>
927

10-
<%= render "docs/_h3.html", title: "Source schema" %>
11-
<p class="lead">Work In Progress 😅</p>
28+
<%= render "docs/_h3.html", title: "Source management" %>
29+
<p class="lead">The Source API let you fetch sources of a project and perform CRUD operations.</p>
30+
<p>Here is how to use the API with TypeScript:</p>
31+
<pre><code class="hljs js">function getSources(orgId: string, projectId: string, authToken: string): Promise&lt;SourceInfo[]> {
32+
return fetch(
33+
`https://azimutt.app/api/v1/organizations/${orgId}/projects/${projectId}/sources`,
34+
{headers: {'auth-token': authToken}}
35+
).then(res => res.json())
36+
}
37+
</code></pre>
38+
39+
<%= render "docs/_h4.html", title: "Automatically update your source" %>
40+
<p>
41+
The main usage for this API is to keep your source always up-to-date with your database.
42+
For example, you can push your database schema to Azimutt at every change using a GitHub action and Azimutt libraries.
43+
</p>
44+
<p>
45+
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
46+
<a href="https://www.npmjs.com/package/@azimutt/models" target="_blank" rel="noopener noreferrer">@azimutt/models</a>:
47+
</p>
48+
<pre><code class="hljs js">import {Database, databaseToSourceContent, LegacySource, LegacySourceContent, parseDatabaseUrl} from "@azimutt/models"
49+
import {postgres} from "@azimutt/connector-postgres"
50+
51+
const azimuttApi = 'https://azimutt.app/api/v1'
52+
const authToken = '2c7cb13d-d2b2-42f8-8eef-f447ab3591db' // from https://azimutt.app/settings
53+
const organizationId = '1749e94e-158a-8419-936f-dc22fff4dac8' // from Azimutt url
54+
const projectId = '2711536a-c539-4096-b685-d42bc93f93fe' // from Azimutt url
55+
const sourceId = '072fd32c-e11d-8a14-a674-0842600f9ae0' // from API source list
56+
const databaseUrl = 'postgresql://postgres:postgres@localhost/azimutt_dev' // you should have it ^^
57+
58+
const database: Database = await postgres.getSchema(
59+
'source updater',
60+
parseDatabaseUrl(databaseUrl),
61+
{logger: {
62+
debug: (text: string): void => console.debug(text),
63+
log: (text: string): void => console.log(text),
64+
warn: (text: string): void => console.warn(text),
65+
error: (text: string): void => console.error(text),
66+
}}
67+
)
68+
const source: LegacySourceContent = databaseToSourceContent(database)
69+
await updateSource(organizationId, projectId, sourceId, source, authToken)
70+
71+
function updateSource(orgId: string, projectId: string, sourceId: string, content: LegacySourceContent, authToken: string): Promise&lt;LegacySource> {
72+
return fetch(`${azimuttApi}/organizations/${orgId}/projects/${projectId}/sources/${sourceId}`, {
73+
method: 'PUT',
74+
headers: {'auth-token': authToken, 'Content-Type': 'application/json'},
75+
body: JSON.stringify(content)
76+
}).then(res => res.json())
77+
}
78+
</code></pre>
1279

1380
<%= render "docs/_h3.html", title: "Schema documentation" %>
14-
<p class="lead">Work In Progress 😅</p>
81+
<p class="lead">The documentation API let you get or set all the documentation from Azimutt.</p>
82+
<p>
83+
A good use case is to sync Azimutt with another documentation tool, having one or the other as a golden source (one way sync).
84+
Here is the minimal code you may need:
85+
</p>
86+
<pre><code class="hljs js">type Metadata = {[tableId: string]: TableDoc & {columns: {[columnPath: string]: ColumnDoc}}}
87+
type TableDoc = {notes?: string, tags?: string[], color?: string}
88+
type ColumnDoc = {notes?: string, tags?: string[]}
89+
90+
function getDocumentation(orgId: string, projectId: string, authToken: string): Promise&lt;Metadata> {
91+
return fetch(`${azimuttApi}/organizations/${orgId}/projects/${projectId}/metadata`, {
92+
headers: {'auth-token': authToken}
93+
}).then(res => res.json())
94+
}
95+
96+
function putDocumentation(orgId: string, projectId: string, doc: Metadata, authToken: string) {
97+
return fetch(`${azimuttApi}/organizations/${orgId}/projects/${projectId}/metadata`, {
98+
method: 'PUT',
99+
headers: {'auth-token': authToken, 'Content-Type': 'application/json'},
100+
body: JSON.stringify(doc)
101+
}).then(res => res.json())
102+
}
103+
104+
const doc = await getDocumentation(orgId, projectId, authToken)
105+
// doc['public.users'] = {notes: 'Some notes', tags: ['api'], color: 'blue', columns: {name: {notes: 'Some notes', tags: ['api']}}}
106+
// delete doc['public.users']
107+
await putDocumentation(orgId, projectId, doc, authToken)
108+
</code></pre>
109+
<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>
15110

16111
<%= render "docs/_h2.html", title: "JavaScript API" %>
17112
<p class="lead">Work In Progress 😅</p>

backend/lib/azimutt_web/utils/project_schema.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ defmodule AzimuttWeb.Utils.ProjectSchema do
1818
"properties" => %{
1919
"notes" => %{"type" => "string"},
2020
"tags" => %{"type" => "array", "items" => %{"type" => "string"}},
21+
"color" => %{"enum" => ["indigo", "violet", "purple", "fuchsia", "pink", "rose", "red", "orange", "amber", "yellow", "lime", "green", "emerald", "teal", "cyan", "sky", "blue"]},
2122
"columns" => %{"type" => "object", "additionalProperties" => @column_meta}
2223
}
2324
}
@@ -128,7 +129,8 @@ defmodule AzimuttWeb.Utils.ProjectSchema do
128129
"comment" => @comment,
129130
"values" => %{"type" => "array", "items" => %{"type" => "string"}},
130131
# MUST include the column inside the `definitions` attribute in the global schema
131-
"columns" => %{"type" => "array", "items" => %{"$ref" => "#/definitions/column"}}
132+
"columns" => %{"type" => "array", "items" => %{"$ref" => "#/definitions/column"}},
133+
"stats" => %{"type" => "object"}
132134
}
133135
}
134136

@@ -140,12 +142,14 @@ defmodule AzimuttWeb.Utils.ProjectSchema do
140142
"schema" => %{"type" => "string"},
141143
"table" => %{"type" => "string"},
142144
"view" => %{"type" => "boolean"},
145+
"definition" => %{"type" => "string"},
143146
"columns" => %{"type" => "array", "items" => @column},
144147
"primaryKey" => @primary_key,
145148
"uniques" => %{"type" => "array", "items" => @unique},
146149
"indexes" => %{"type" => "array", "items" => @index},
147150
"checks" => %{"type" => "array", "items" => @check},
148-
"comment" => @comment
151+
"comment" => @comment,
152+
"stats" => %{"type" => "object"}
149153
}
150154
}
151155

11 KB
Loading

gateway/package-lock.json

Lines changed: 25 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gateway/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"@azimutt/connector-postgres": "^0.1.11",
3737
"@azimutt/connector-snowflake": "^0.1.2",
3838
"@azimutt/connector-sqlserver": "^0.1.4",
39-
"@azimutt/models": "^0.1.17",
39+
"@azimutt/models": "^0.1.19",
4040
"@azimutt/utils": "^0.1.8",
4141
"@fastify/cors": "9.0.1",
4242
"@sinclair/typebox": "0.29.6",

libs/models/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@azimutt/models",
33
"description": "Define a standard database models for Azimutt.",
4-
"version": "0.1.18",
4+
"version": "0.1.19",
55
"license": "MIT",
66
"homepage": "https://azimutt.app",
77
"keywords": [],

libs/models/src/databaseUtils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ export function attributeValueToString(value: AttributeValue): string {
268268
if (typeof value === 'number') return value.toString()
269269
if (typeof value === 'boolean') return value.toString()
270270
if (value instanceof Date) return value.toISOString()
271+
if (value === undefined) return 'null'
271272
if (value === null) return 'null'
272273
return JSON.stringify(value)
273274
}

0 commit comments

Comments
 (0)