dbdiagram 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/COPYRIGHT.md +3 -0
  2. package/README.md +189 -0
  3. package/dist/actions/auth/auth-login.action.js +44 -0
  4. package/dist/actions/auth/auth-logout.action.js +13 -0
  5. package/dist/actions/auth/auth-status.action.js +38 -0
  6. package/dist/actions/build/build-document.action.js +142 -0
  7. package/dist/actions/delete.action.js +67 -0
  8. package/dist/actions/init.action.js +105 -0
  9. package/dist/actions/list/list-document.action.js +130 -0
  10. package/dist/actions/list/list.action.js +103 -0
  11. package/dist/actions/pull.action.js +58 -0
  12. package/dist/actions/push.action.js +91 -0
  13. package/dist/actions/tokens/token-delete.action.js +50 -0
  14. package/dist/actions/tokens/token-generate.action.js +52 -0
  15. package/dist/actions/tokens/token-list.action.js +52 -0
  16. package/dist/assets/callback-error.html +109 -0
  17. package/dist/assets/callback-success.html +73 -0
  18. package/dist/commands/auth/auth-login.command.js +7 -0
  19. package/dist/commands/auth/auth-logout.command.js +7 -0
  20. package/dist/commands/auth/auth-status.command.js +8 -0
  21. package/dist/commands/auth/auth.command.js +16 -0
  22. package/dist/commands/build/build-document.command.js +23 -0
  23. package/dist/commands/build/build.command.js +10 -0
  24. package/dist/commands/command.loader.js +18 -0
  25. package/dist/commands/delete.command.js +12 -0
  26. package/dist/commands/init.command.js +14 -0
  27. package/dist/commands/list/list-document.command.js +11 -0
  28. package/dist/commands/list/list.command.js +15 -0
  29. package/dist/commands/pull.command.js +11 -0
  30. package/dist/commands/push.command.js +15 -0
  31. package/dist/commands/tokens/token-delete.command.js +9 -0
  32. package/dist/commands/tokens/token-generate.command.js +9 -0
  33. package/dist/commands/tokens/token-list.command.js +8 -0
  34. package/dist/commands/tokens/tokens.command.js +15 -0
  35. package/dist/config/credential-manager.js +52 -0
  36. package/dist/config/settings-manager.js +44 -0
  37. package/dist/config.js +14 -0
  38. package/dist/config.staging.js +14 -0
  39. package/dist/constants/auth-message.constant.js +1 -0
  40. package/dist/constants/document.constant.js +8 -0
  41. package/dist/constants/dot-dbdiagram-dir.constant.js +9 -0
  42. package/dist/constants/viz-read-error.constant.js +6 -0
  43. package/dist/errors/portal-api.error.js +37 -0
  44. package/dist/errors/portal-error-codes.js +23 -0
  45. package/dist/hooks/build-document.hook.js +30 -0
  46. package/dist/hooks/delete.hook.js +12 -0
  47. package/dist/hooks/global.hook.js +6 -0
  48. package/dist/hooks/list.hook.js +23 -0
  49. package/dist/hooks/pull.hook.js +15 -0
  50. package/dist/hooks/push.hook.js +19 -0
  51. package/dist/index.js +12 -0
  52. package/dist/integrations/portal/portal-http.integration.js +27 -0
  53. package/dist/integrations/portal/portal.integration.js +68 -0
  54. package/dist/libs/portal/client.js +114 -0
  55. package/dist/libs/portal/errors.js +20 -0
  56. package/dist/libs/portal/index.js +2 -0
  57. package/dist/libs/portal/server.js +70 -0
  58. package/dist/program.js +16 -0
  59. package/dist/services/dbml/dbml.service.js +30 -0
  60. package/dist/services/dbml/dbml.worker.js +40 -0
  61. package/dist/services/file.service.js +6 -0
  62. package/dist/services/viz/diagram-viz-converter.service.js +67 -0
  63. package/dist/services/viz/diagram-viz-file.service.js +43 -0
  64. package/dist/types/config.type.js +1 -0
  65. package/dist/types/diagram-viz.type.js +6 -0
  66. package/dist/types/integrations/portal.type.js +1 -0
  67. package/dist/utils/dbml.util.js +30 -0
  68. package/dist/utils/diagram-viz-error.util.js +11 -0
  69. package/dist/utils/logger.util.js +3 -0
  70. package/dist/utils/output.util.js +18 -0
  71. package/dist/utils/portal-error.util.js +18 -0
  72. package/dist/utils/table.util.js +10 -0
  73. package/dist/utils/validation.util.js +32 -0
  74. package/package.json +56 -0
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import { load } from './commands/command.loader.js';
4
+ import { createProgram } from './program.js';
5
+ import { globalPreAction } from './hooks/global.hook.js';
6
+ const bootstrap = async () => {
7
+ const program = createProgram();
8
+ load(program);
9
+ program.hook('preAction', globalPreAction);
10
+ await program.parseAsync(process.argv);
11
+ };
12
+ bootstrap();
@@ -0,0 +1,27 @@
1
+ import axios from 'axios';
2
+ import { getAccessToken } from '../../config/credential-manager.js';
3
+ import { PORTAL_URLS } from '../../config.js';
4
+ import { PortalApiError } from '../../errors/portal-api.error.js';
5
+ const axiosInstance = axios.create({
6
+ baseURL: `${PORTAL_URLS.apiUrl}/portal`,
7
+ headers: {
8
+ 'Content-Type': 'application/json; charset=utf-8',
9
+ Accept: 'application/json; charset=utf-8',
10
+ },
11
+ });
12
+ axiosInstance.interceptors.request.use((config) => {
13
+ const token = getAccessToken();
14
+ if (token) {
15
+ config.headers.Authorization = token;
16
+ }
17
+ return config;
18
+ }, (error) => {
19
+ return Promise.reject(error);
20
+ });
21
+ axiosInstance.interceptors.response.use((response) => response, (error) => {
22
+ if (axios.isAxiosError(error)) {
23
+ return Promise.reject(PortalApiError.fromAxios(error));
24
+ }
25
+ return Promise.reject(error);
26
+ });
27
+ export { axiosInstance as portalHttpClient };
@@ -0,0 +1,68 @@
1
+ import { portalHttpClient } from './portal-http.integration.js';
2
+ const CLI_BASE_PATH = '/cli';
3
+ async function getCurrentUser() {
4
+ const { data } = await portalHttpClient.get(`${CLI_BASE_PATH}/me`);
5
+ return data;
6
+ }
7
+ async function createDiagram(req) {
8
+ const { name, dbml, workspaceId, vizData } = req;
9
+ const params = workspaceId ? { workspace: workspaceId } : undefined;
10
+ const { data } = await portalHttpClient.post(`${CLI_BASE_PATH}/diagrams`, { name, dbml, vizData }, { params });
11
+ return data;
12
+ }
13
+ async function createDocument(req) {
14
+ const { data } = await portalHttpClient.post(`${CLI_BASE_PATH}/projects`, req);
15
+ return data;
16
+ }
17
+ async function updateDiagram(req) {
18
+ const { name, dbml, vizData } = req;
19
+ const { data } = await portalHttpClient.put(`${CLI_BASE_PATH}/diagrams/${req.diagramId}`, { name, dbml, vizData });
20
+ return data;
21
+ }
22
+ async function getDiagram(diagramId) {
23
+ const { data } = await portalHttpClient.get(`${CLI_BASE_PATH}/diagrams/${diagramId}`);
24
+ return data;
25
+ }
26
+ async function deleteDiagram(diagramId) {
27
+ await portalHttpClient.delete(`${CLI_BASE_PATH}/diagrams/${diagramId}`);
28
+ return;
29
+ }
30
+ async function listDiagrams(workspaceId) {
31
+ const params = workspaceId ? { workspace: workspaceId } : undefined;
32
+ const { data } = await portalHttpClient.get(`${CLI_BASE_PATH}/diagrams`, { params });
33
+ return data;
34
+ }
35
+ async function listWorkspaces() {
36
+ const { data } = await portalHttpClient.get(`${CLI_BASE_PATH}/workspaces`);
37
+ return data;
38
+ }
39
+ async function listWorkspaceProjects(name) {
40
+ const { data } = await portalHttpClient.get(`/cli/workspaces/${name}/projects`);
41
+ return data;
42
+ }
43
+ async function generateToken(req) {
44
+ const { data } = await portalHttpClient.post(`${CLI_BASE_PATH}/tokens`, req);
45
+ return data;
46
+ }
47
+ async function listTokens() {
48
+ const { data } = await portalHttpClient.get(`${CLI_BASE_PATH}/tokens`);
49
+ return data;
50
+ }
51
+ async function deleteToken(id) {
52
+ await portalHttpClient.delete(`/cli/tokens/${id}`);
53
+ return;
54
+ }
55
+ export const portalIntegration = {
56
+ getCurrentUser,
57
+ createDiagram,
58
+ updateDiagram,
59
+ createDocument,
60
+ getDiagram,
61
+ deleteDiagram,
62
+ listDiagrams,
63
+ listWorkspaces,
64
+ listWorkspaceProjects,
65
+ generateToken,
66
+ listTokens,
67
+ deleteToken,
68
+ };
@@ -0,0 +1,114 @@
1
+ import { createHash, randomBytes } from 'crypto';
2
+ import axios from 'axios';
3
+ import { PORTAL_AUTH_ERROR_CODES, PortalAuthenticationError } from './errors.js';
4
+ import { startLoopbackServer } from './server.js';
5
+ const STATE_BYTES_LENGTH = 32;
6
+ const CODE_VERIFIER_BYTES_LENGTH = 32;
7
+ const generateLoginState = () => randomBytes(STATE_BYTES_LENGTH).toString('base64url');
8
+ const generateCodeVerifier = () => randomBytes(CODE_VERIFIER_BYTES_LENGTH).toString('base64url');
9
+ const generateCodeChallenge = (codeVerifier) => {
10
+ const hash = createHash('sha256');
11
+ hash.update(codeVerifier);
12
+ return hash.digest().toString('base64url');
13
+ };
14
+ const generateLoginUrl = (params) => {
15
+ const { baseUrl, redirectUri, codeChallenge, state, clientId } = params;
16
+ const url = new URL('/login', baseUrl);
17
+ url.searchParams.set('client_id', clientId);
18
+ url.searchParams.set('redirect_uri', redirectUri);
19
+ url.searchParams.set('response_type', 'code');
20
+ url.searchParams.set('code_challenge', codeChallenge);
21
+ url.searchParams.set('state', state);
22
+ url.searchParams.set('prompt', 'select_account');
23
+ return url;
24
+ };
25
+ async function exchangeCodeForToken(params) {
26
+ const { apiHost, code, codeVerifier, redirectUri, clientId } = params;
27
+ try {
28
+ const { data } = await axios.post(`${apiHost}/portal/auth/token`, {
29
+ grantType: 'authorization_code',
30
+ redirectUri,
31
+ clientId,
32
+ code,
33
+ codeVerifier,
34
+ });
35
+ return data;
36
+ }
37
+ catch (error) {
38
+ if (axios.isAxiosError(error)) {
39
+ const description = error.response
40
+ ? error.response.data?.message || ''
41
+ : error.message || '';
42
+ throw new PortalAuthenticationError(PORTAL_AUTH_ERROR_CODES.exchangeFailed, description);
43
+ }
44
+ throw new PortalAuthenticationError(PORTAL_AUTH_ERROR_CODES.exchangeFailed, error instanceof Error ? error.message : '');
45
+ }
46
+ }
47
+ export async function initAuthClient(options) {
48
+ const { loginUrl, loginApiUrl, clientId, loginTimeoutMs, callbackHtmlPaths } = options;
49
+ const codeVerifier = generateCodeVerifier();
50
+ const codeChallenge = generateCodeChallenge(codeVerifier);
51
+ const state = generateLoginState();
52
+ const { baseUrl, close: closeLoopbackServer, loginCompletePromise } = await startLoopbackServer({
53
+ callbackHtmlPaths,
54
+ });
55
+ const redirectUri = `${baseUrl}/callback`;
56
+ const portalLoginUrl = generateLoginUrl({
57
+ baseUrl: loginUrl,
58
+ redirectUri,
59
+ codeChallenge,
60
+ state,
61
+ clientId,
62
+ });
63
+ const getLoginUrl = () => portalLoginUrl.toString();
64
+ const obtainTokens = async () => {
65
+ let timeoutId;
66
+ let sigintHandler;
67
+ try {
68
+ const timeoutPromise = new Promise((_, reject) => {
69
+ timeoutId = setTimeout(() => {
70
+ reject(new PortalAuthenticationError(PORTAL_AUTH_ERROR_CODES.loginTimeout));
71
+ }, loginTimeoutMs);
72
+ });
73
+ const cancellationPromise = new Promise((_, reject) => {
74
+ sigintHandler = () => {
75
+ reject(new PortalAuthenticationError(PORTAL_AUTH_ERROR_CODES.loginCancelled));
76
+ };
77
+ process.once('SIGINT', sigintHandler);
78
+ });
79
+ const { code, state: returnedState } = await Promise.race([
80
+ loginCompletePromise,
81
+ timeoutPromise,
82
+ cancellationPromise,
83
+ ]);
84
+ if (state !== returnedState) {
85
+ throw new PortalAuthenticationError(PORTAL_AUTH_ERROR_CODES.stateMismatch);
86
+ }
87
+ const { accessToken } = await exchangeCodeForToken({
88
+ apiHost: loginApiUrl,
89
+ code,
90
+ codeVerifier,
91
+ redirectUri,
92
+ clientId,
93
+ });
94
+ return { accessToken };
95
+ }
96
+ catch (error) {
97
+ if (error instanceof PortalAuthenticationError)
98
+ throw error;
99
+ throw new PortalAuthenticationError(PORTAL_AUTH_ERROR_CODES.unknownError, error instanceof Error ? error.message : '');
100
+ }
101
+ finally {
102
+ if (timeoutId)
103
+ clearTimeout(timeoutId);
104
+ if (sigintHandler)
105
+ process.removeListener('SIGINT', sigintHandler);
106
+ closeLoopbackServer();
107
+ }
108
+ };
109
+ return {
110
+ getLoginUrl,
111
+ obtainTokens,
112
+ cleanup: () => { closeLoopbackServer(); },
113
+ };
114
+ }
@@ -0,0 +1,20 @@
1
+ export const PORTAL_AUTH_ERROR_CODES = {
2
+ loginTimeout: 'LOGIN_TIMEOUT',
3
+ loginCancelled: 'LOGIN_CANCELLED',
4
+ stateMismatch: 'STATE_MISMATCH',
5
+ invalidCallback: 'INVALID_CALLBACK',
6
+ exchangeFailed: 'EXCHANGE_FAILED',
7
+ loopbackServerError: 'LOOPBACK_SERVER_ERROR',
8
+ unknownError: 'UNKNOWN_ERROR',
9
+ };
10
+ export class PortalAuthenticationError extends Error {
11
+ errorCode;
12
+ errorDescription;
13
+ constructor(errorCode, errorDescription = '') {
14
+ super(errorCode);
15
+ this.name = 'PortalAuthenticationError';
16
+ this.errorCode = errorCode;
17
+ this.errorDescription = errorDescription;
18
+ Error.captureStackTrace?.(this, PortalAuthenticationError);
19
+ }
20
+ }
@@ -0,0 +1,2 @@
1
+ export { initAuthClient } from './client.js';
2
+ export { PORTAL_AUTH_ERROR_CODES, PortalAuthenticationError } from './errors.js';
@@ -0,0 +1,70 @@
1
+ import fs from 'fs';
2
+ import { createServer } from 'http';
3
+ import { PORTAL_AUTH_ERROR_CODES, PortalAuthenticationError } from './errors.js';
4
+ const LOOPBACK_ADDRESS = '127.0.0.1';
5
+ const AUTO_ASSIGNED_PORT = 0;
6
+ const DEFAULT_HTTP_HEADERS = {
7
+ 'Content-Type': 'text/html; charset=utf-8',
8
+ };
9
+ export const startLoopbackServer = ({ callbackHtmlPaths }) => {
10
+ const successHtml = fs.readFileSync(callbackHtmlPaths.success, 'utf-8');
11
+ const errorHtml = fs.readFileSync(callbackHtmlPaths.error, 'utf-8');
12
+ return new Promise((resolve, reject) => {
13
+ let isServerClosed = false;
14
+ let loginResolve;
15
+ let loginReject;
16
+ const loginCompletePromise = new Promise((res, rej) => {
17
+ loginResolve = res;
18
+ loginReject = rej;
19
+ });
20
+ const server = createServer();
21
+ const closeServer = () => {
22
+ if (!isServerClosed && server) {
23
+ isServerClosed = true;
24
+ server.closeAllConnections();
25
+ server.close();
26
+ }
27
+ };
28
+ server.on('error', (err) => {
29
+ closeServer();
30
+ reject(new PortalAuthenticationError(PORTAL_AUTH_ERROR_CODES.loopbackServerError, err.message || ''));
31
+ });
32
+ server.on('request', (req, res) => {
33
+ const reqUrl = new URL(req.url ?? '', `http://${req.headers.host}`);
34
+ const { pathname } = reqUrl;
35
+ switch (pathname) {
36
+ case '/callback': {
37
+ const code = reqUrl.searchParams.get('code');
38
+ const state = reqUrl.searchParams.get('state');
39
+ if (!code || !state) {
40
+ res.writeHead(400, DEFAULT_HTTP_HEADERS);
41
+ res.end(errorHtml);
42
+ closeServer();
43
+ loginReject(new PortalAuthenticationError(PORTAL_AUTH_ERROR_CODES.invalidCallback));
44
+ return;
45
+ }
46
+ res.writeHead(200, DEFAULT_HTTP_HEADERS);
47
+ res.end(successHtml);
48
+ loginResolve({ code, state });
49
+ break;
50
+ }
51
+ default: {
52
+ res.writeHead(404, DEFAULT_HTTP_HEADERS);
53
+ res.end();
54
+ closeServer();
55
+ loginReject(new PortalAuthenticationError(PORTAL_AUTH_ERROR_CODES.invalidCallback));
56
+ return;
57
+ }
58
+ }
59
+ });
60
+ server.listen(AUTO_ASSIGNED_PORT, LOOPBACK_ADDRESS, () => {
61
+ const { port } = server.address();
62
+ resolve({
63
+ port,
64
+ baseUrl: `http://${LOOPBACK_ADDRESS}:${port}`,
65
+ loginCompletePromise,
66
+ close: closeServer,
67
+ });
68
+ });
69
+ });
70
+ };
@@ -0,0 +1,16 @@
1
+ import { Command } from 'commander';
2
+ import packageJson from '../package.json' with { type: 'json' };
3
+ export function createProgram() {
4
+ const program = new Command();
5
+ program
6
+ .name('dbdiagram')
7
+ .description('CLI tool for working with the dbx ecosystem (dbdiagram, dbdocs)')
8
+ .version(`${packageJson.name} ${packageJson.version}`, '-v, --version', 'Output the version number')
9
+ .helpOption('-h, --help', 'Show help information')
10
+ .enablePositionalOptions(true)
11
+ .action(() => {
12
+ program.help();
13
+ })
14
+ .showHelpAfterError(true);
15
+ return program;
16
+ }
@@ -0,0 +1,30 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { Worker } from 'node:worker_threads';
4
+ const workerPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'dbml.worker.js');
5
+ function parseDbml(content) {
6
+ return new Promise((resolve, reject) => {
7
+ const worker = new Worker(workerPath, { execArgv: process.execArgv });
8
+ worker.postMessage(content);
9
+ worker.on('message', (msg) => {
10
+ worker.terminate();
11
+ if (!msg.error) {
12
+ resolve(msg.data);
13
+ return;
14
+ }
15
+ const { message, diags } = msg.error;
16
+ const err = new Error(message);
17
+ if (diags) {
18
+ err.diags = msg.error.diags;
19
+ }
20
+ reject(err);
21
+ });
22
+ worker.on('error', reject);
23
+ });
24
+ }
25
+ export function prepareBuildDocumentData(content) {
26
+ return parseDbml(content);
27
+ }
28
+ export function preparePushDiagramData(content) {
29
+ return parseDbml(content);
30
+ }
@@ -0,0 +1,40 @@
1
+ import { parentPort } from 'node:worker_threads';
2
+ import { Parser } from '@dbml/core';
3
+ import removeMd from 'remove-markdown';
4
+ parentPort?.on('message', (content) => {
5
+ try {
6
+ const parser = new Parser();
7
+ const database = parser.parse(content, 'dbmlv2');
8
+ const { name: dbName = '', note: dbDescription = '' } = database;
9
+ const name = dbName.trim();
10
+ const description = (dbDescription ?? '').trim() !== '' ? removeMd(dbDescription).trim() : '';
11
+ const shallowSchema = database.schemas.map((schema) => ({
12
+ name: schema.name,
13
+ tables: schema.tables.map((table) => table.name),
14
+ }));
15
+ const normalizedDatabase = database.normalize();
16
+ const diagramViews = database.diagramViews.map((view) => ({
17
+ name: view.name,
18
+ visibleEntities: view.visibleEntities,
19
+ }));
20
+ parentPort?.postMessage({
21
+ data: {
22
+ name,
23
+ description,
24
+ shallowSchema,
25
+ normalizedDatabase,
26
+ database,
27
+ diagramViews,
28
+ },
29
+ });
30
+ }
31
+ catch (error) {
32
+ const { diags } = error;
33
+ parentPort?.postMessage({
34
+ error: {
35
+ message: error instanceof Error ? error.message : String(error),
36
+ ...(diags !== undefined ? { diags } : {}),
37
+ },
38
+ });
39
+ }
40
+ });
@@ -0,0 +1,6 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ export const writeFileWithDir = async (filePath, data, encoding = 'utf8') => {
4
+ await mkdir(dirname(filePath), { recursive: true });
5
+ await writeFile(filePath, data, encoding);
6
+ };
@@ -0,0 +1,67 @@
1
+ import { DIAGRAM_VIZ_FILE_VERSIONS } from '../../constants/dot-dbdiagram-dir.constant.js';
2
+ export const migrateDiagramVizV1ToV2 = (v1) => {
3
+ return {
4
+ version: DIAGRAM_VIZ_FILE_VERSIONS.v2,
5
+ darkMode: v1.darkMode,
6
+ gridEnabling: v1.gridEnabling,
7
+ currentViewName: null,
8
+ defaultView: {
9
+ detailLevel: v1.detailLevel,
10
+ tablePositions: v1.tablePositions,
11
+ tableGroupCollapseStates: v1.tableGroupCollapseStates,
12
+ stickyNoteLayouts: v1.stickyNoteLayouts,
13
+ referencePaths: v1.referencePaths,
14
+ },
15
+ views: {},
16
+ };
17
+ };
18
+ export const toServerFormat = (state, diagramViews) => {
19
+ const { defaultView, views } = state;
20
+ const viewEntitiesMap = new Map(diagramViews?.map((v) => [v.name, v.visibleEntities]));
21
+ return {
22
+ diagram: { tables: defaultView.tablePositions },
23
+ referencePaths: defaultView.referencePaths,
24
+ stickyNoteLayouts: defaultView.stickyNoteLayouts,
25
+ tableGroups: defaultView.tableGroupCollapseStates,
26
+ detailLevel: defaultView.detailLevel,
27
+ visibleEntities: viewEntitiesMap.get('Default'),
28
+ views: Object.entries(views).map(([name, view]) => ({
29
+ name,
30
+ detailLevel: view.detailLevel,
31
+ tablePositions: view.tablePositions,
32
+ tableGroupCollapsedStates: view.tableGroupCollapseStates,
33
+ stickyNoteLayouts: view.stickyNoteLayouts,
34
+ referencePaths: view.referencePaths,
35
+ visibleEntities: viewEntitiesMap.get(name),
36
+ })),
37
+ };
38
+ };
39
+ const viewFromServer = (view) => {
40
+ return {
41
+ detailLevel: (view.detailLevel ?? ''),
42
+ tablePositions: view.tablePositions ?? [],
43
+ tableGroupCollapseStates: view.tableGroupCollapsedStates ?? [],
44
+ stickyNoteLayouts: view.stickyNoteLayouts ?? [],
45
+ referencePaths: view.referencePaths ?? [],
46
+ };
47
+ };
48
+ export const fromServerFormat = (serverData, existing) => {
49
+ const views = {};
50
+ for (const view of serverData.views) {
51
+ views[view.name] = viewFromServer(view);
52
+ }
53
+ return {
54
+ version: DIAGRAM_VIZ_FILE_VERSIONS.v2,
55
+ darkMode: existing?.darkMode ?? true,
56
+ gridEnabling: existing?.gridEnabling ?? false,
57
+ currentViewName: existing?.currentViewName ?? null,
58
+ defaultView: {
59
+ detailLevel: serverData.detailLevel,
60
+ tablePositions: serverData.diagram.tables,
61
+ tableGroupCollapseStates: serverData.tableGroups,
62
+ stickyNoteLayouts: serverData.stickyNoteLayouts,
63
+ referencePaths: serverData.referencePaths,
64
+ },
65
+ views,
66
+ };
67
+ };
@@ -0,0 +1,43 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { writeFileWithDir } from '../file.service.js';
3
+ import { DBML_EXTENSION, DIAGRAM_VIZ_EXTENSION, DIAGRAM_VIZ_FILE_VERSIONS, } from '../../constants/dot-dbdiagram-dir.constant.js';
4
+ import { DIAGRAM_VIZ_READ_ERROR } from '../../constants/viz-read-error.constant.js';
5
+ import { migrateDiagramVizV1ToV2 } from './diagram-viz-converter.service.js';
6
+ export const deriveDiagramVizPathFromDBML = (dbmlFilePath) => {
7
+ const base = dbmlFilePath.endsWith(DBML_EXTENSION)
8
+ ? dbmlFilePath.slice(0, -DBML_EXTENSION.length)
9
+ : dbmlFilePath;
10
+ return `${base}${DIAGRAM_VIZ_EXTENSION}`;
11
+ };
12
+ export const readDiagramVizFile = async (vizPath) => {
13
+ let raw;
14
+ try {
15
+ raw = await readFile(vizPath, 'utf8');
16
+ }
17
+ catch (error) {
18
+ if (error.code === 'ENOENT') {
19
+ return { ok: false, errorCode: DIAGRAM_VIZ_READ_ERROR.file_not_found };
20
+ }
21
+ return { ok: false, errorCode: DIAGRAM_VIZ_READ_ERROR.read_error };
22
+ }
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(raw);
26
+ }
27
+ catch {
28
+ return { ok: false, errorCode: DIAGRAM_VIZ_READ_ERROR.invalid_json };
29
+ }
30
+ const { version } = parsed;
31
+ if (version === DIAGRAM_VIZ_FILE_VERSIONS.v2) {
32
+ return { ok: true, state: parsed };
33
+ }
34
+ if (version === undefined || version === DIAGRAM_VIZ_FILE_VERSIONS.v1) {
35
+ const migrated = migrateDiagramVizV1ToV2(parsed);
36
+ await writeDiagramVizFile(vizPath, migrated);
37
+ return { ok: true, state: migrated };
38
+ }
39
+ return { ok: false, errorCode: DIAGRAM_VIZ_READ_ERROR.unknown_version };
40
+ };
41
+ export const writeDiagramVizFile = async (vizPath, state) => {
42
+ await writeFileWithDir(vizPath, JSON.stringify(state, null, 2));
43
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export var DetailLevel;
2
+ (function (DetailLevel) {
3
+ DetailLevel["Tables"] = "Tables";
4
+ DetailLevel["Keys"] = "Keys";
5
+ DetailLevel["All"] = "All";
6
+ })(DetailLevel || (DetailLevel = {}));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ export function getDbmlVersion() {
5
+ const require = createRequire(import.meta.url);
6
+ const entry = require.resolve('@dbml/core');
7
+ const pkg = JSON.parse(readFileSync(path.resolve(path.dirname(entry), '..', 'package.json'), 'utf8'));
8
+ return pkg.version;
9
+ }
10
+ export function parseDbmlDiags(error) {
11
+ if (!error || typeof error !== 'object' || !Array.isArray(error.diags)) {
12
+ return [];
13
+ }
14
+ const { diags } = error;
15
+ return diags
16
+ .map((d) => {
17
+ const loc = d.location?.start;
18
+ const locStr = loc ? ` (line ${loc.line}, column ${loc.column})` : '';
19
+ return d.message ? `${d.message}${locStr}` : '';
20
+ })
21
+ .filter(Boolean);
22
+ }
23
+ export function formatDbmlError(error) {
24
+ const diags = parseDbmlDiags(error);
25
+ if (diags.length > 0)
26
+ return `\n - ${diags.join('\n - ')}`;
27
+ if (error instanceof Error)
28
+ return error.message;
29
+ return String(error);
30
+ }
@@ -0,0 +1,11 @@
1
+ import { DIAGRAM_VIZ_READ_ERROR } from '../constants/viz-read-error.constant.js';
2
+ const VIZ_READ_ERROR_MESSAGES = {
3
+ [DIAGRAM_VIZ_READ_ERROR.read_error]: (path) => `Failed to read viz file ${path}, skipping viz data.`,
4
+ [DIAGRAM_VIZ_READ_ERROR.invalid_json]: (path) => `Viz file ${path} is not valid JSON, skipping viz data.`,
5
+ [DIAGRAM_VIZ_READ_ERROR.file_not_found]: (path) => `Viz file ${path} is not found, skipping viz data.`,
6
+ [DIAGRAM_VIZ_READ_ERROR.unknown_version]: (path) => `Viz file ${path} has an unrecognized version, skipping viz data.`,
7
+ };
8
+ export const getVizReadErrorMessage = (errorCode, vizPath) => {
9
+ const mapping = VIZ_READ_ERROR_MESSAGES[errorCode];
10
+ return mapping ? mapping(vizPath) : `Unknown error while reading diagram viz file ${vizPath}`;
11
+ };
@@ -0,0 +1,3 @@
1
+ export const logConsole = (...args) => {
2
+ console.log(...args);
3
+ };
@@ -0,0 +1,18 @@
1
+ import Table from 'cli-table3';
2
+ import { logConsole } from './logger.util.js';
3
+ export function printJson(data) {
4
+ logConsole(JSON.stringify(data, null, 2));
5
+ }
6
+ export function printTable(data, config) {
7
+ const table = new Table({
8
+ head: config.head,
9
+ style: { head: [] },
10
+ colWidths: config.colWidths,
11
+ wordWrap: true,
12
+ wrapOnWordBoundary: false,
13
+ });
14
+ for (const item of data) {
15
+ table.push(config.rowMapper(item));
16
+ }
17
+ logConsole(table.toString());
18
+ }
@@ -0,0 +1,18 @@
1
+ import { PortalApiError } from '../errors/portal-api.error.js';
2
+ import { PORTAL_ERROR_MESSAGES, VALIDATION_INVALID_PAYLOAD } from '../errors/portal-error-codes.js';
3
+ export function isPortalApiError(error) {
4
+ return error instanceof PortalApiError;
5
+ }
6
+ export function getPortalApiErrorMessage(error) {
7
+ if (error.errorCode.startsWith('auth_')) {
8
+ return [
9
+ 'Authentication failed: your credentials are invalid or expired. Try one of:',
10
+ ' - Replace DBDIAGRAM_TOKEN with a valid token',
11
+ ' - Unset DBDIAGRAM_TOKEN, then run `dbdiagram auth login` to re-authenticate',
12
+ ].join('\n');
13
+ }
14
+ if (error.errorCode === VALIDATION_INVALID_PAYLOAD) {
15
+ return error.message;
16
+ }
17
+ return PORTAL_ERROR_MESSAGES[error.errorCode] ?? `An unexpected error occurred [${error.errorCode}]`;
18
+ }
@@ -0,0 +1,10 @@
1
+ export function computeColWidths(fractions) {
2
+ const terminalWidth = process.stdout.columns || 100;
3
+ const numCols = fractions.length;
4
+ const overhead = numCols * 3 + 1;
5
+ const available = Math.max(terminalWidth - overhead, numCols * 5);
6
+ const widths = fractions.map((f) => Math.max(Math.floor(available * f), 5));
7
+ const used = widths.reduce((a, b) => a + b, 0);
8
+ widths[widths.length - 1] += available - used;
9
+ return widths;
10
+ }