diff --git a/backend/lib/azimutt_web/controllers/api/source_controller.ex b/backend/lib/azimutt_web/controllers/api/source_controller.ex index ecc4a4d0b..70104168a 100644 --- a/backend/lib/azimutt_web/controllers/api/source_controller.ex +++ b/backend/lib/azimutt_web/controllers/api/source_controller.ex @@ -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) @@ -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 diff --git a/backend/lib/azimutt_web/templates/website/docs/_h4.html.heex b/backend/lib/azimutt_web/templates/website/docs/_h4.html.heex new file mode 100644 index 000000000..d265c63af --- /dev/null +++ b/backend/lib/azimutt_web/templates/website/docs/_h4.html.heex @@ -0,0 +1,4 @@ +
Work In Progress 😅
++ 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: +
+Work In Progress 😅
-See Swagger spec.
+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.
+Check the whole Swagger spec for available endpoints, the two main parts are:
+ +If you miss some endpoints, let us know and we will add them.
+
+ The API authentication with made through a custom header token that will impersonate you.
+ You can create one from your user settings (see "Your authentication tokens" at the bottom, hover the gray box to see it).
+ Then put it on a auth-token
request header, it will authenticate you through the API.
+
Work In Progress 😅
+ <%= render "docs/_h3.html", title: "Source management" %> +The Source API let you fetch sources of a project and perform CRUD operations.
+Here is how to use the API with TypeScript:
+function getSources(orgId: string, projectId: string, authToken: string): Promise<SourceInfo[]> {
+ return fetch(
+ `https://azimutt.app/api/v1/organizations/${orgId}/projects/${projectId}/sources`,
+ {headers: {'auth-token': authToken}}
+ ).then(res => res.json())
+}
+
+
+ <%= render "docs/_h4.html", title: "Automatically update your source" %>
+ + 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. +
++ Here is a sample script for PostgreSQL using @azimutt/connector-postgres and + @azimutt/models: +
+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<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())
+}
+
<%= render "docs/_h3.html", title: "Schema documentation" %>
- Work In Progress 😅
+The documentation API let you get or set all the documentation from Azimutt.
++ 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: +
+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<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)
+
+ These methods get and update the whole documentation, there is also endpoints for a specific table or column if you need more granular access.
<%= render "docs/_h2.html", title: "JavaScript API" %>Work In Progress 😅
diff --git a/backend/lib/azimutt_web/utils/project_schema.ex b/backend/lib/azimutt_web/utils/project_schema.ex index 75584cb9c..b63dc4b53 100644 --- a/backend/lib/azimutt_web/utils/project_schema.ex +++ b/backend/lib/azimutt_web/utils/project_schema.ex @@ -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} } } @@ -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"} } } @@ -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"} } } diff --git a/backend/priv/static/images/doc/api-user-token.png b/backend/priv/static/images/doc/api-user-token.png new file mode 100644 index 000000000..61a94e0d3 Binary files /dev/null and b/backend/priv/static/images/doc/api-user-token.png differ diff --git a/gateway/package-lock.json b/gateway/package-lock.json index 04ec4d14c..b643a0e34 100644 --- a/gateway/package-lock.json +++ b/gateway/package-lock.json @@ -18,7 +18,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", @@ -1104,9 +1104,9 @@ } }, "node_modules/@azimutt/models": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/@azimutt/models/-/models-0.1.17.tgz", - "integrity": "sha512-AKpAoUyU1cz3icPUekRESwveGT9ei1usQneIOXVjwWK3byamOyCfPCBwVHX6ACKZs9jUw8N2ZltNM5iLeQLxhA==", + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@azimutt/models/-/models-0.1.19.tgz", + "integrity": "sha512-tyGK5STdKnFUr8pPQl9ivLdcyXEpBwBIvHwTh9Pf+x/Mf4GwhR35xzePaA49AFgNLCLK0oLvUHBPwTeHQQ1Mbg==", "license": "MIT", "dependencies": { "@azimutt/utils": "^0.1.8", @@ -3303,9 +3303,10 @@ } }, "node_modules/@types/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", "dependencies": { "@types/node": "*", "form-data": "^4.0.0" @@ -3751,9 +3752,10 @@ } }, "node_modules/agentkeepalive": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", - "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", "dependencies": { "humanize-ms": "^1.2.1" }, @@ -5189,12 +5191,14 @@ "node_modules/form-data-encoder": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" }, "node_modules/formdata-node": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" @@ -5484,6 +5488,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", "dependencies": { "ms": "^2.0.0" } @@ -6304,6 +6309,7 @@ "url": "https://paypal.me/jimmywarting" } ], + "license": "MIT", "engines": { "node": ">=10.5.0" } @@ -6409,6 +6415,7 @@ "version": "4.65.0", "resolved": "https://registry.npmjs.org/openai/-/openai-4.65.0.tgz", "integrity": "sha512-LfA4KUBpH/8rA3vjCQ74LZtdK/8wx9W6Qxq8MHqEdImPsN1XPQ2ompIuJWkKS6kXt5Cs5i8Eb65IIo4M7U+yeQ==", + "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -6431,9 +6438,10 @@ } }, "node_modules/openai/node_modules/@types/node": { - "version": "18.19.54", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.54.tgz", - "integrity": "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw==", + "version": "18.19.71", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.71.tgz", + "integrity": "sha512-evXpcgtZm8FY4jqBSN8+DmOTcVkkvTmAayeo4Wf3m1xAruyVGzGuDh/Fb/WWX2yLItUiho42ozyJjB0dw//Tkw==", + "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } @@ -8475,6 +8483,7 @@ "version": "4.0.0-beta.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", "engines": { "node": ">= 14" } @@ -8662,6 +8671,7 @@ "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -8670,6 +8680,7 @@ "version": "3.23.3", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.3.tgz", "integrity": "sha512-TYWChTxKQbRJp5ST22o/Irt9KC5nj7CdBKYB/AosCRdj/wxEMvv4NNaj9XVUHDOIp53ZxArGhnw5HMZziPFjog==", + "license": "ISC", "peerDependencies": { "zod": "^3.23.3" } diff --git a/gateway/package.json b/gateway/package.json index 73c176871..c56136696 100644 --- a/gateway/package.json +++ b/gateway/package.json @@ -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", diff --git a/libs/models/package.json b/libs/models/package.json index 4e72cb943..f8fb355c0 100644 --- a/libs/models/package.json +++ b/libs/models/package.json @@ -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": [], diff --git a/libs/models/src/databaseUtils.ts b/libs/models/src/databaseUtils.ts index 298a4dc5e..4a3dba87b 100644 --- a/libs/models/src/databaseUtils.ts +++ b/libs/models/src/databaseUtils.ts @@ -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) } diff --git a/libs/models/src/legacy/legacyDatabase.ts b/libs/models/src/legacy/legacyDatabase.ts index b51dc8243..8e93e8824 100644 --- a/libs/models/src/legacy/legacyDatabase.ts +++ b/libs/models/src/legacy/legacyDatabase.ts @@ -4,6 +4,7 @@ import { Attribute, AttributePath, AttributeRef, + AttributeStats, AttributeValue, Check, Database, @@ -15,6 +16,7 @@ import { Relation, Type } from "../database"; +import {attributeValueToString} from "../databaseUtils"; import {ValueSchema} from "../inferSchema"; import {DateTime} from "../common"; @@ -295,13 +297,7 @@ function columnToLegacy(a: Attribute): LegacyColumn { comment: a.doc, values: a.stats?.distinctValues?.map(columnValueToLegacy), columns: a.attrs?.map(columnToLegacy), - stats: a.stats ? removeUndefined({ - nulls: a.stats.nulls, - bytesAvg: a.stats.bytesAvg, - cardinality: a.stats.cardinality, - commonValues: a.stats.commonValues?.map(v => ({value: columnValueToLegacy(v.value), freq: v.freq})), - histogram: a.stats.histogram?.map(columnValueToLegacy), - }) : undefined, + stats: a.stats ? attributeDbStatsToLegacy(a.stats) : undefined, }) } @@ -310,10 +306,17 @@ export function columnValueFromLegacy(v: LegacyColumnValue): AttributeValue { } export function columnValueToLegacy(v: AttributeValue): LegacyColumnValue { - if (v === undefined) return 'null' - if (v === null) return 'null' - if (typeof v === 'object') return JSON.stringify(v) - return v.toString() // TODO: improve? + return attributeValueToString(v) +} + +export function attributeDbStatsToLegacy(s: AttributeStats): LegacyColumnDbStats { + return removeUndefined({ + nulls: s.nulls, + bytesAvg: s.bytesAvg, + cardinality: s.cardinality, + commonValues: s.commonValues?.map(v => ({value: columnValueToLegacy(v.value), freq: v.freq})), + histogram: s.histogram?.map(columnValueToLegacy), + }) } export function primaryKeyFromLegacy(pk: LegacyPrimaryKey): PrimaryKey { @@ -322,7 +325,7 @@ export function primaryKeyFromLegacy(pk: LegacyPrimaryKey): PrimaryKey { attrs: pk.columns.map(columnNameFromLegacy) }) } -function primaryKeyToLegacy(pk: PrimaryKey): LegacyPrimaryKey { +export function primaryKeyToLegacy(pk: PrimaryKey): LegacyPrimaryKey { return removeUndefined({ name: pk.name, columns: pk.attrs.map(columnNameToLegacy) @@ -333,7 +336,7 @@ export function columnNameFromLegacy(n: LegacyColumnName): AttributePath { return n.split(legacyColumnPathSeparator) } -function columnNameToLegacy(p: AttributePath): LegacyColumnName { +export function columnNameToLegacy(p: AttributePath): LegacyColumnName { return p.join(legacyColumnPathSeparator) } @@ -346,7 +349,7 @@ export function uniqueFromLegacy(u: LegacyUnique): Index { }) } -function uniqueToLegacy(i: Index): LegacyUnique { +export function uniqueToLegacy(i: Index): LegacyUnique { return removeUndefined({ name: i.name, columns: i.attrs.map(columnNameToLegacy), @@ -362,7 +365,7 @@ export function indexFromLegacy(i: LegacyIndex): Index { }) } -function indexToLegacy(i: Index): LegacyIndex { +export function indexToLegacy(i: Index): LegacyIndex { return removeUndefined({ name: i.name, columns: i.attrs.map(columnNameToLegacy), @@ -378,7 +381,7 @@ export function checkFromLegacy(c: LegacyCheck): Check { }) } -function checkToLegacy(c: Check): LegacyCheck { +export function checkToLegacy(c: Check): LegacyCheck { return removeUndefined({ name: c.name, columns: c.attrs.map(columnNameToLegacy), diff --git a/libs/models/src/legacy/legacyProject.ts b/libs/models/src/legacy/legacyProject.ts index 6b633429b..acbfea002 100644 --- a/libs/models/src/legacy/legacyProject.ts +++ b/libs/models/src/legacy/legacyProject.ts @@ -1,8 +1,10 @@ import {z} from "zod"; -import {groupBy, removeEmpty, removeUndefined} from "@azimutt/utils"; +import {groupBy, removeEmpty, removeUndefined, zip} from "@azimutt/utils"; import {Color, Position, Size, Slug, Timestamp, Uuid} from "../common"; import { + attributeDbStatsToLegacy, checkFromLegacy, + columnNameToLegacy, columnValueFromLegacy, indexFromLegacy, LegacyColumnDbStats, @@ -18,11 +20,13 @@ import { primaryKeyFromLegacy, relationFromLegacy, tableDbStatsFromLegacy, + tableDbStatsToLegacy, uniqueFromLegacy } from "./legacyDatabase"; import {zodParse} from "../zod"; -import {Attribute, Database, DatabaseKind, Entity, Type} from "../database"; +import {Attribute, Check, Database, DatabaseKind, Entity, Index, PrimaryKey, Relation, Type} from "../database"; import {parseDatabaseUrl} from "../databaseUrl"; +import {attributeValueToString} from "../databaseUtils"; import {OpenAIKey, OpenAIModel} from "../llm"; // MUST stay in sync with backend/lib/azimutt_web/utils/project_schema.ex @@ -257,7 +261,7 @@ export const LegacyProjectCheck = z.object({ origins: LegacyOrigin.array().optional() }).strict() -// TODO: mutualise with LegacyTable in libs/models/src/legacy/legacyDatabase.ts:80 +// TODO: mutualise with LegacyTable in libs/models/src/legacy/legacyDatabase.ts:80 (incompatible `comment` field :/) export interface LegacyProjectTable { schema: LegacySchemaName table: LegacyTableName @@ -340,6 +344,18 @@ export interface LegacySource { updatedAt: Timestamp } +export const LegacySourceContent = z.object({ + tables: LegacyProjectTable.array(), + relations: LegacyProjectRelation.array(), + types: LegacyProjectType.array().optional(), +}).strict() + +export interface LegacySourceContent { + tables: LegacyProjectTable[] + relations: LegacyProjectRelation[] + types?: LegacyProjectType[] +} + export const LegacySource = z.object({ id: LegacySourceId, name: LegacySourceName, @@ -899,6 +915,14 @@ export function sourceToDatabase(s: LegacySource): Database { }) } +export function databaseToSourceContent(db: Database): LegacySourceContent { + return removeUndefined({ + tables: (db.entities || []).map(entityToProjectTable), + relations: (db.relations || []).flatMap(relationToProjectRelation), + types: db.types?.map(typeToProjectType), + }) +} + export function projectTableToEntity(t: LegacyProjectTable): Entity { return removeUndefined({ database: undefined, @@ -917,6 +941,58 @@ export function projectTableToEntity(t: LegacyProjectTable): Entity { }) } +export function entityToProjectTable(e: Entity): LegacyProjectTable { + const uniques = (e.indexes || []).filter(i => i.unique).map(uniqueToProjectLegacy) + const indexes = (e.indexes || []).filter(i => !i.unique).map(indexToProjectLegacy) + const checks = (e.checks || []).map(checkToProjectLegacy) + return removeUndefined({ + schema: e.schema || '', + table: e.name, + view: e.kind !== undefined ? e.kind !== 'table' : undefined, + definition: e.def, + columns: (e.attrs || []).map(attributeToProjectColumn), + primaryKey: e.pk ? primaryKeyToProjectLegacy(e.pk) : undefined, + uniques: uniques.length > 0 ? uniques : undefined, + indexes: indexes.length > 0 ? indexes : undefined, + checks: checks.length > 0 ? checks: undefined, + comment: e.doc ? {text: e.doc} : undefined, + stats: e.stats ? tableDbStatsToLegacy(e.stats) : undefined, + origins: undefined, + }) +} + +export function primaryKeyToProjectLegacy(pk: PrimaryKey): LegacyProjectPrimaryKey { + return removeUndefined({ + name: pk.name, + columns: pk.attrs.map(columnNameToLegacy), + origins: undefined, + }) +} + +export function uniqueToProjectLegacy(i: Index): LegacyProjectUnique { + return removeUndefined({ + name: i.name || '', + columns: i.attrs.map(columnNameToLegacy), + definition: i.definition + }) +} + +export function indexToProjectLegacy(i: Index): LegacyProjectIndex { + return removeUndefined({ + name: i.name || '', + columns: i.attrs.map(columnNameToLegacy), + definition: i.definition + }) +} + +export function checkToProjectLegacy(c: Check): LegacyProjectCheck { + return removeUndefined({ + name: c.name || '', + columns: c.attrs.map(columnNameToLegacy), + predicate: c.predicate || undefined + }) +} + export function projectColumnToAttribute(c: LegacyProjectColumn): Attribute { return removeEmpty({ name: c.name, @@ -939,6 +1015,31 @@ export function projectColumnToAttribute(c: LegacyProjectColumn): Attribute { }) } +export function attributeToProjectColumn(a: Attribute): LegacyProjectColumn { + return removeUndefined({ + name: a.name, + type: a.type, + nullable: a.null, + default: a.default ? attributeValueToString(a.default) : undefined, + comment: a.doc ? {text: a.doc} : undefined, + // values?: string[] + columns: a.attrs?.map(attributeToProjectColumn), + stats: a.stats ? attributeDbStatsToLegacy(a.stats) : undefined, + origins: undefined, + }) +} + +export function relationToProjectRelation(r: Relation): LegacyProjectRelation[] { + return zip(r.src.attrs, r.ref.attrs).map(([src, ref]) => { + return removeUndefined({ + name: r.name || '', + src: { table: `${r.src.schema}.${r.src.entity}`, column: src.join(':') }, + ref: { table: `${r.ref.schema}.${r.ref.entity}`, column: ref.join(':') }, + origins: undefined, + }) + }) +} + export function projectTypeFromLegacy(t: LegacyProjectType): Type { if ('enum' in t.value) { return removeUndefined({schema: t.schema || undefined, name: t.name, values: t.value.enum || undefined}) @@ -946,3 +1047,12 @@ export function projectTypeFromLegacy(t: LegacyProjectType): Type { return removeUndefined({schema: t.schema || undefined, name: t.name, definition: t.value.definition}) } } + +export function typeToProjectType(t: Type): LegacyProjectType { + return removeUndefined({ + schema: t.schema || '', + name: t.name, + value: t.values ? {enum: t.values} : t.definition ? {definition: t.definition} : {definition: ''}, + origins: undefined, + }) +} diff --git a/libs/utils/src/logger.ts b/libs/utils/src/logger.ts index c78e5de38..94f9e9c1b 100644 --- a/libs/utils/src/logger.ts +++ b/libs/utils/src/logger.ts @@ -4,3 +4,10 @@ export interface Logger { warn: (text: string) => void error: (text: string) => void } + +export const loggerNoOp: Logger = { + debug: (text: string): void => {}, + log: (text: string): void => {}, + warn: (text: string): void => {}, + error: (text: string): void => {} +}