dbdiagram 0.1.2 → 0.3.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 (42) hide show
  1. package/README.md +13 -0
  2. package/dist/actions/auth/auth-login.action.js +8 -19
  3. package/dist/actions/auth/auth-logout.action.js +11 -3
  4. package/dist/actions/auth/auth-status.action.js +7 -13
  5. package/dist/actions/build/build-document.action.js +29 -45
  6. package/dist/actions/delete.action.js +8 -15
  7. package/dist/actions/list/list-document.action.js +13 -19
  8. package/dist/actions/list/list.action.js +12 -19
  9. package/dist/actions/pull.action.js +8 -17
  10. package/dist/actions/push.action.js +22 -24
  11. package/dist/actions/tokens/token-delete.action.js +8 -14
  12. package/dist/actions/tokens/token-generate.action.js +7 -12
  13. package/dist/actions/tokens/token-list.action.js +10 -17
  14. package/dist/commands/auth/auth-logout.command.js +1 -0
  15. package/dist/commands/auth/auth.command.js +1 -1
  16. package/dist/commands/build/build.command.js +1 -1
  17. package/dist/commands/tokens/tokens.command.js +1 -1
  18. package/dist/config/credential-manager.js +10 -23
  19. package/dist/config/settings-manager.js +14 -31
  20. package/dist/config.js +4 -0
  21. package/dist/constants/cli-error.constant.js +81 -0
  22. package/dist/constants/dot-dbdiagram-dir.constant.js +1 -0
  23. package/dist/errors/cli.error.js +9 -0
  24. package/dist/errors/portal-api.error.js +1 -0
  25. package/dist/hooks/global.hook.js +8 -1
  26. package/dist/index.js +22 -2
  27. package/dist/integrations/telemetry/telemetry.integration.js +77 -0
  28. package/dist/program.js +2 -1
  29. package/dist/services/telemetry.service.js +59 -0
  30. package/dist/types/telemetry.type.js +1 -0
  31. package/dist/utils/agent-detector.util.js +27 -0
  32. package/dist/utils/cli-error.util.js +46 -0
  33. package/dist/utils/command-name.util.js +12 -0
  34. package/dist/utils/date.util.js +4 -0
  35. package/dist/utils/environment-detector.util.js +6 -0
  36. package/dist/utils/file.util.js +31 -0
  37. package/dist/utils/output.util.js +35 -4
  38. package/package.json +4 -2
  39. package/dist/constants/auth-message.constant.js +0 -1
  40. package/dist/errors/portal-error-codes.js +0 -23
  41. package/dist/utils/portal-error.util.js +0 -18
  42. package/dist/utils/table.util.js +0 -10
@@ -1,28 +1,27 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { log, spinner } from '@clack/prompts';
2
+ import { spinner } from '@clack/prompts';
3
3
  import { getCredential } from '../config/credential-manager.js';
4
- import { NOT_LOGGED_IN_MESSAGE } from '../constants/auth-message.constant.js';
5
4
  import { portalIntegration } from '../integrations/portal/portal.integration.js';
5
+ import { trackEvent } from '../services/telemetry.service.js';
6
6
  import { deriveDiagramVizPathFromDBML, readDiagramVizFile } from '../services/viz/diagram-viz-file.service.js';
7
- import { getVizReadErrorMessage } from '../utils/diagram-viz-error.util.js';
8
7
  import { toServerFormat } from '../services/viz/diagram-viz-converter.service.js';
9
8
  import { preparePushDiagramData } from '../services/dbml/dbml.service.js';
10
9
  import { DBDIAGRAM_CONFIGS } from '../config.js';
11
- import { isPortalApiError, getPortalApiErrorMessage } from '../utils/portal-error.util.js';
12
- import { printJson } from '../utils/output.util.js';
10
+ import { CliError } from '../errors/cli.error.js';
11
+ import { getCliErrorMessageFromCode, toCliError } from '../utils/cli-error.util.js';
12
+ import { AUTH_NOT_LOGGED_IN, FILE_PATH_MISSING, FILE_READ_ERROR, DBML_PARSE_ERROR, } from '../constants/cli-error.constant.js';
13
+ import { displayError, getOutputFormat, printJson } from '../utils/output.util.js';
13
14
  import { formatDbmlError } from '../utils/dbml.util.js';
14
15
  export async function pushAction(filepath, options, thisCommand) {
15
16
  const credential = getCredential();
16
17
  if (!credential) {
17
- log.error(NOT_LOGGED_IN_MESSAGE);
18
18
  process.exitCode = 1;
19
- return;
19
+ return displayError(new CliError(AUTH_NOT_LOGGED_IN, getCliErrorMessageFromCode(AUTH_NOT_LOGGED_IN)), getOutputFormat(options.json));
20
20
  }
21
21
  const resolvedFilepath = thisCommand.args[0] || filepath;
22
22
  if (!resolvedFilepath) {
23
- log.error('Missing required argument: filepath. Provide it as an argument or set dbml.entry in settings.');
24
23
  process.exitCode = 1;
25
- return;
24
+ return displayError(new CliError(FILE_PATH_MISSING, getCliErrorMessageFromCode(FILE_PATH_MISSING)), getOutputFormat(options.json));
26
25
  }
27
26
  let dbml;
28
27
  try {
@@ -30,18 +29,16 @@ export async function pushAction(filepath, options, thisCommand) {
30
29
  }
31
30
  catch (error) {
32
31
  const message = error instanceof Error ? error.message : String(error);
33
- log.error(`Failed to read file: ${message}`);
34
32
  process.exitCode = 1;
35
- return;
33
+ return displayError(new CliError(FILE_READ_ERROR, `Failed to read file: ${message}`), getOutputFormat(options.json));
36
34
  }
37
35
  let preparedPushData;
38
36
  try {
39
37
  preparedPushData = await preparePushDiagramData(dbml);
40
38
  }
41
39
  catch (error) {
42
- log.error(`Failed to parse DBML: ${formatDbmlError(error)}`);
43
40
  process.exitCode = 1;
44
- return;
41
+ return displayError(new CliError(DBML_PARSE_ERROR, `Failed to parse DBML: ${formatDbmlError(error)}`), getOutputFormat(options.json));
45
42
  }
46
43
  const vizPath = deriveDiagramVizPathFromDBML(resolvedFilepath);
47
44
  const vizResult = await readDiagramVizFile(vizPath);
@@ -50,23 +47,30 @@ export async function pushAction(filepath, options, thisCommand) {
50
47
  const { diagramViews } = preparedPushData;
51
48
  vizData = toServerFormat(vizResult.state, diagramViews);
52
49
  }
53
- else {
54
- const message = getVizReadErrorMessage(vizResult.errorCode, vizPath);
55
- log.warn(message);
56
- }
57
50
  const s = spinner();
58
51
  s.start('Pushing diagram to dbdiagram...');
59
52
  try {
53
+ const currentUser = await portalIntegration.getCurrentUser();
60
54
  let diagramId;
61
55
  if (!options.new && options.diagramId) {
62
56
  ({ diagramId } = await portalIntegration.updateDiagram({
63
57
  diagramId: options.diagramId, name: options.name, dbml, vizData,
64
58
  }));
59
+ trackEvent({
60
+ name: 'cli_diagram_push',
61
+ properties: { action: 'update', diagramId, workspaceId: options.workspace ?? null },
62
+ userId: currentUser.id.toString(),
63
+ });
65
64
  }
66
65
  else {
67
66
  ({ diagramId } = await portalIntegration.createDiagram({
68
67
  name: options.name, dbml, workspaceId: options.workspace, vizData,
69
68
  }));
69
+ trackEvent({
70
+ name: 'cli_diagram_push',
71
+ properties: { action: 'create', diagramId, workspaceId: options.workspace ?? null },
72
+ userId: currentUser.id.toString(),
73
+ });
70
74
  }
71
75
  const diagramUrl = `${DBDIAGRAM_CONFIGS.baseUrl}/d/${diagramId}`;
72
76
  if (options.json) {
@@ -79,13 +83,7 @@ export async function pushAction(filepath, options, thisCommand) {
79
83
  }
80
84
  catch (error) {
81
85
  s.stop('Failed to push diagram.');
82
- if (isPortalApiError(error)) {
83
- log.error(getPortalApiErrorMessage(error));
84
- }
85
- else {
86
- const message = error instanceof Error ? error.message : String(error);
87
- log.error(message);
88
- }
86
+ displayError(toCliError(error), getOutputFormat(options.json));
89
87
  process.exitCode = 1;
90
88
  }
91
89
  }
@@ -1,21 +1,20 @@
1
- import { confirm, isCancel, log, spinner } from '@clack/prompts';
1
+ import { confirm, isCancel, spinner } from '@clack/prompts';
2
2
  import { getCredential } from '../../config/credential-manager.js';
3
- import { NOT_LOGGED_IN_MESSAGE } from '../../constants/auth-message.constant.js';
3
+ import { CliError } from '../../errors/cli.error.js';
4
+ import { getCliErrorMessageFromCode, toCliError } from '../../utils/cli-error.util.js';
5
+ import { AUTH_NOT_LOGGED_IN, CONFIRMATION_REQUIRED } from '../../constants/cli-error.constant.js';
4
6
  import { portalIntegration } from '../../integrations/portal/portal.integration.js';
5
- import { isPortalApiError, getPortalApiErrorMessage } from '../../utils/portal-error.util.js';
6
- import { printJson } from '../../utils/output.util.js';
7
+ import { displayError, getOutputFormat, printJson } from '../../utils/output.util.js';
7
8
  export async function tokenDeleteAction(tokenId, options) {
8
9
  const credential = getCredential();
9
10
  if (!credential) {
10
- log.error(NOT_LOGGED_IN_MESSAGE);
11
11
  process.exitCode = 1;
12
- return;
12
+ return displayError(new CliError(AUTH_NOT_LOGGED_IN, getCliErrorMessageFromCode(AUTH_NOT_LOGGED_IN)), getOutputFormat(options.json));
13
13
  }
14
14
  if (!options.force) {
15
15
  if (!process.stdout.isTTY) {
16
- log.error('Use -f to force delete in non-interactive environment.');
17
16
  process.exitCode = 1;
18
- return;
17
+ return displayError(new CliError(CONFIRMATION_REQUIRED, getCliErrorMessageFromCode(CONFIRMATION_REQUIRED)), getOutputFormat(options.json));
19
18
  }
20
19
  const shouldDelete = await confirm({
21
20
  message: `Delete token '${tokenId}'?`,
@@ -39,12 +38,7 @@ export async function tokenDeleteAction(tokenId, options) {
39
38
  }
40
39
  catch (error) {
41
40
  s.stop('Failed to delete token.');
42
- if (isPortalApiError(error)) {
43
- log.error(getPortalApiErrorMessage(error));
44
- }
45
- else {
46
- log.error(error instanceof Error ? error.message : String(error));
47
- }
41
+ displayError(toCliError(error), getOutputFormat(options.json));
48
42
  process.exitCode = 1;
49
43
  }
50
44
  }
@@ -1,12 +1,13 @@
1
1
  import os from 'node:os';
2
- import { log, spinner } from '@clack/prompts';
2
+ import { spinner } from '@clack/prompts';
3
3
  import packageJson from '../../../package.json' with { type: 'json' };
4
4
  import { getCredential } from '../../config/credential-manager.js';
5
- import { NOT_LOGGED_IN_MESSAGE } from '../../constants/auth-message.constant.js';
5
+ import { AUTH_NOT_LOGGED_IN } from '../../constants/cli-error.constant.js';
6
+ import { CliError } from '../../errors/cli.error.js';
6
7
  import { portalIntegration } from '../../integrations/portal/portal.integration.js';
7
- import { isPortalApiError, getPortalApiErrorMessage } from '../../utils/portal-error.util.js';
8
- import { printJson } from '../../utils/output.util.js';
8
+ import { getCliErrorMessageFromCode, toCliError } from '../../utils/cli-error.util.js';
9
9
  import { logConsole } from '../../utils/logger.util.js';
10
+ import { displayError, getOutputFormat, printJson } from '../../utils/output.util.js';
10
11
  const PLATFORM_NAMES = {
11
12
  darwin: 'macOS',
12
13
  win32: 'Windows',
@@ -19,9 +20,8 @@ export function generateDefaultTokenName() {
19
20
  export async function tokenGenerateAction(opts) {
20
21
  const credential = getCredential();
21
22
  if (!credential) {
22
- log.error(NOT_LOGGED_IN_MESSAGE);
23
23
  process.exitCode = 1;
24
- return;
24
+ return displayError(new CliError(AUTH_NOT_LOGGED_IN, getCliErrorMessageFromCode(AUTH_NOT_LOGGED_IN)), getOutputFormat(opts.json));
25
25
  }
26
26
  const name = opts.name ?? generateDefaultTokenName();
27
27
  const s = spinner();
@@ -41,12 +41,7 @@ export async function tokenGenerateAction(opts) {
41
41
  }
42
42
  catch (error) {
43
43
  s.stop('Failed to generate token.');
44
- if (isPortalApiError(error)) {
45
- log.error(getPortalApiErrorMessage(error));
46
- }
47
- else {
48
- log.error(error instanceof Error ? error.message : String(error));
49
- }
44
+ displayError(toCliError(error), getOutputFormat(opts.json));
50
45
  process.exitCode = 1;
51
46
  }
52
47
  }
@@ -1,17 +1,16 @@
1
- import { log, spinner } from '@clack/prompts';
2
- import { DateTime } from 'luxon';
1
+ import { spinner } from '@clack/prompts';
3
2
  import { getCredential } from '../../config/credential-manager.js';
4
- import { NOT_LOGGED_IN_MESSAGE } from '../../constants/auth-message.constant.js';
3
+ import { AUTH_NOT_LOGGED_IN } from '../../constants/cli-error.constant.js';
4
+ import { CliError } from '../../errors/cli.error.js';
5
5
  import { portalIntegration } from '../../integrations/portal/portal.integration.js';
6
- import { isPortalApiError, getPortalApiErrorMessage } from '../../utils/portal-error.util.js';
7
- import { computeColWidths } from '../../utils/table.util.js';
8
- import { printJson, printTable } from '../../utils/output.util.js';
6
+ import { getCliErrorMessageFromCode, toCliError } from '../../utils/cli-error.util.js';
7
+ import { formatShortDateTime } from '../../utils/date.util.js';
8
+ import { displayError, getOutputFormat, printJson, printTable } from '../../utils/output.util.js';
9
9
  export async function tokenListAction(opts) {
10
10
  const credential = getCredential();
11
11
  if (!credential) {
12
- log.error(NOT_LOGGED_IN_MESSAGE);
13
12
  process.exitCode = 1;
14
- return;
13
+ return displayError(new CliError(AUTH_NOT_LOGGED_IN, getCliErrorMessageFromCode(AUTH_NOT_LOGGED_IN)), getOutputFormat(opts.json));
15
14
  }
16
15
  const s = spinner();
17
16
  s.start('Fetching tokens...');
@@ -25,8 +24,8 @@ export async function tokenListAction(opts) {
25
24
  const mappedData = tokens.map((t) => ({
26
25
  id: t.id,
27
26
  name: t.name,
28
- createdAt: DateTime.fromISO(t.createdAt).toLocaleString(DateTime.DATETIME_SHORT),
29
- lastUsedAt: t.lastUsedAt ? DateTime.fromISO(t.lastUsedAt).toLocaleString(DateTime.DATETIME_SHORT) : 'Never',
27
+ createdAt: formatShortDateTime(t.createdAt),
28
+ lastUsedAt: t.lastUsedAt ? formatShortDateTime(t.lastUsedAt) : 'Never',
30
29
  }));
31
30
  if (opts.json) {
32
31
  printJson(mappedData);
@@ -34,19 +33,13 @@ export async function tokenListAction(opts) {
34
33
  else {
35
34
  printTable(mappedData, {
36
35
  head: ['ID', 'Name', 'Created at', 'Last used'],
37
- colWidths: computeColWidths([0.2, 0.45, 0.17, 0.18]),
38
36
  rowMapper: (t) => [t.id, t.name, t.createdAt, t.lastUsedAt],
39
37
  });
40
38
  }
41
39
  }
42
40
  catch (error) {
43
41
  s.stop('Failed to fetch tokens.');
44
- if (isPortalApiError(error)) {
45
- log.error(getPortalApiErrorMessage(error));
46
- }
47
- else {
48
- log.error(error instanceof Error ? error.message : String(error));
49
- }
42
+ displayError(toCliError(error), getOutputFormat(opts.json));
50
43
  process.exitCode = 1;
51
44
  }
52
45
  }
@@ -3,5 +3,6 @@ export function registerLogoutCommand(auth) {
3
3
  auth
4
4
  .command('logout')
5
5
  .description('Clear the local stored credentials and log out')
6
+ .option('--json', 'Output as JSON')
6
7
  .action(logoutAction);
7
8
  }
@@ -8,7 +8,7 @@ export function registerAuthCommand(program) {
8
8
  .addHelpText('after', '\nAlternatively, set the DBDIAGRAM_TOKEN env var to authenticate with a\n' +
9
9
  'CLI token (see: dbdiagram tokens generate).')
10
10
  .action(() => {
11
- auth.help();
11
+ auth.outputHelp();
12
12
  });
13
13
  registerLoginCommand(auth);
14
14
  registerLogoutCommand(auth);
@@ -4,7 +4,7 @@ export function registerBuildCommand(program) {
4
4
  .command('build')
5
5
  .description('Publish documentation to dbdocs.io')
6
6
  .action(() => {
7
- build.help();
7
+ build.outputHelp();
8
8
  });
9
9
  registerDocumentCommand(build);
10
10
  }
@@ -7,7 +7,7 @@ export function registerTokensCommand(program) {
7
7
  .description('Manage your CLI tokens')
8
8
  .addHelpText('after', '\nSet a token value as DBDIAGRAM_TOKEN env var to authenticate without browser login.')
9
9
  .action(() => {
10
- tokens.help();
10
+ tokens.outputHelp();
11
11
  });
12
12
  registerTokenGenerateCommand(tokens);
13
13
  registerTokenListCommand(tokens);
@@ -1,40 +1,27 @@
1
1
  import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { DOT_DBDIAGRAM_DIR, CREDENTIALS_FILE_NAME } from '../constants/dot-dbdiagram-dir.constant.js';
2
+ import { getGlobalCredentialsFilePath, getGlobalDir, getProjectCredentialsFilePath, readAndParseJsonFileSafe, } from '../utils/file.util.js';
5
3
  let cachedCredential = null;
6
- function readJsonSafe(filePath) {
7
- try {
8
- const contents = fs.readFileSync(filePath, 'utf8');
9
- return JSON.parse(contents);
10
- }
11
- catch {
12
- return null;
13
- }
14
- }
15
4
  export function resolveCredential() {
16
5
  if (process.env.DBDIAGRAM_TOKEN) {
17
6
  cachedCredential = { token: process.env.DBDIAGRAM_TOKEN };
18
7
  return cachedCredential;
19
8
  }
20
- const projectPath = path.join(process.cwd(), DOT_DBDIAGRAM_DIR, CREDENTIALS_FILE_NAME);
21
- const projectCred = readJsonSafe(projectPath);
22
- if (projectCred && typeof projectCred.token === 'string') {
23
- cachedCredential = { token: projectCred.token };
9
+ const projectCredential = readAndParseJsonFileSafe(getProjectCredentialsFilePath());
10
+ if (projectCredential && typeof projectCredential.token === 'string') {
11
+ cachedCredential = { token: projectCredential.token };
24
12
  return cachedCredential;
25
13
  }
26
- const userPath = path.join(os.homedir(), DOT_DBDIAGRAM_DIR, CREDENTIALS_FILE_NAME);
27
- const userCred = readJsonSafe(userPath);
28
- if (userCred && typeof userCred.token === 'string') {
29
- cachedCredential = { token: userCred.token };
14
+ const globalCredential = readAndParseJsonFileSafe(getGlobalCredentialsFilePath());
15
+ if (globalCredential && typeof globalCredential.token === 'string') {
16
+ cachedCredential = { token: globalCredential.token };
30
17
  return cachedCredential;
31
18
  }
32
19
  cachedCredential = null;
33
20
  return null;
34
21
  }
35
22
  export function storeCredential(token) {
36
- const dir = path.join(os.homedir(), DOT_DBDIAGRAM_DIR);
37
- const filePath = path.join(dir, CREDENTIALS_FILE_NAME);
23
+ const dir = getGlobalDir();
24
+ const filePath = getGlobalCredentialsFilePath();
38
25
  fs.mkdirSync(dir, { recursive: true });
39
26
  fs.writeFileSync(filePath, JSON.stringify({ token }, null, 2), { mode: 0o600 });
40
27
  cachedCredential = { token };
@@ -46,7 +33,7 @@ export function getAccessToken() {
46
33
  return cachedCredential?.token ?? null;
47
34
  }
48
35
  export function clearCredential() {
49
- const filePath = path.join(os.homedir(), DOT_DBDIAGRAM_DIR, CREDENTIALS_FILE_NAME);
36
+ const filePath = getGlobalCredentialsFilePath();
50
37
  fs.rmSync(filePath, { force: true });
51
38
  cachedCredential = null;
52
39
  }
@@ -1,12 +1,8 @@
1
1
  import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { DOT_DBDIAGRAM_DIR, SETTINGS_FILE_NAME } from '../constants/dot-dbdiagram-dir.constant.js';
2
+ import { getProjectDir, getSettingsFilePath, readAndParseJsonFileSafe } from '../utils/file.util.js';
4
3
  let cachedSettings = null;
5
- function settingsPath() {
6
- return path.join(process.cwd(), DOT_DBDIAGRAM_DIR, SETTINGS_FILE_NAME);
7
- }
8
4
  export function settingsExists() {
9
- return fs.existsSync(settingsPath());
5
+ return fs.existsSync(getSettingsFilePath());
10
6
  }
11
7
  export function defaultSettings() {
12
8
  return {
@@ -15,30 +11,17 @@ export function defaultSettings() {
15
11
  document: { source: 'file', workspaceUrlName: '', projectUrlName: '' },
16
12
  };
17
13
  }
18
- export function readSettings() {
19
- try {
20
- const contents = fs.readFileSync(settingsPath(), 'utf8');
21
- return JSON.parse(contents);
22
- }
23
- catch {
24
- return null;
25
- }
26
- }
27
- export function writeSettings(data) {
28
- const dir = path.join(process.cwd(), DOT_DBDIAGRAM_DIR);
29
- fs.mkdirSync(dir, { recursive: true });
30
- fs.writeFileSync(settingsPath(), JSON.stringify(data, null, 2), 'utf8');
14
+ export const readSettings = () => {
15
+ return readAndParseJsonFileSafe(getSettingsFilePath());
16
+ };
17
+ export const writeSettings = (data) => {
18
+ fs.mkdirSync(getProjectDir(), { recursive: true });
19
+ fs.writeFileSync(getSettingsFilePath(), JSON.stringify(data, null, 2), 'utf8');
31
20
  cachedSettings = data;
32
- }
33
- export function resolveSettings() {
34
- try {
35
- const contents = fs.readFileSync(settingsPath(), 'utf8');
36
- cachedSettings = JSON.parse(contents);
37
- }
38
- catch {
39
- cachedSettings = null;
40
- }
41
- }
42
- export function getSettings() {
21
+ };
22
+ export const resolveSettings = () => {
23
+ cachedSettings = readAndParseJsonFileSafe(getSettingsFilePath());
24
+ };
25
+ export const getSettings = () => {
43
26
  return cachedSettings;
44
- }
27
+ };
package/dist/config.js CHANGED
@@ -12,3 +12,7 @@ export const DBDIAGRAM_CONFIGS = {
12
12
  export const DBDOCS_CONFIGS = {
13
13
  baseUrl: process.env.DBDOCS_BASE_URL || 'https://dbdocs.io',
14
14
  };
15
+ export const TELEMETRY_CONFIGS = {
16
+ collectorUrl: process.env.TRACKING_COLLECTOR_URL || 'https://snowplow.holistics.io',
17
+ appId: process.env.TRACKING_APP_ID || 'dbdiagram-cli',
18
+ };
@@ -0,0 +1,81 @@
1
+ export const AUTH_NOT_LOGGED_IN = 'auth_not_logged_in';
2
+ export const AUTH_INVALID_CREDENTIALS = 'auth_invalid_credentials';
3
+ export const AUTH_LOGIN_TIMEOUT = 'auth_login_timeout';
4
+ export const AUTH_LOGIN_CANCELLED = 'auth_login_cancelled';
5
+ export const AUTH_LOGIN_STATE_MISMATCH = 'auth_login_state_mismatch';
6
+ export const AUTH_LOGIN_INVALID_CALLBACK = 'auth_login_invalid_callback';
7
+ export const AUTH_LOGIN_EXCHANGE_FAILED = 'auth_login_exchange_failed';
8
+ export const AUTH_LOGIN_SERVER_ERROR = 'auth_login_server_error';
9
+ export const AUTH_LOGIN_FAILED = 'auth_login_failed';
10
+ export const AUTH_LOGOUT_FAILED = 'auth_logout_failed';
11
+ export const DIAGRAM_ID_MISSING = 'diagram_id_missing';
12
+ export const DIAGRAM_NOT_FOUND = 'diagram_not_found';
13
+ export const DIAGRAM_PERMISSION_DENIED = 'diagram_permission_denied';
14
+ export const DIAGRAM_LIMIT_EXCEEDED = 'diagram_limit_exceeded';
15
+ export const WORKSPACE_NAME_MISSING = 'workspace_name_missing';
16
+ export const WORKSPACE_NAME_INVALID = 'workspace_name_invalid';
17
+ export const WORKSPACE_NOT_FOUND = 'workspace_not_found';
18
+ export const WORKSPACE_PERMISSION_DENIED = 'workspace_permission_denied';
19
+ export const PROJECT_NAME_MISSING = 'project_name_missing';
20
+ export const PROJECT_NAME_INVALID = 'project_name_invalid';
21
+ export const VERSION_NAME_INVALID = 'version_name_invalid';
22
+ export const TOKEN_NOT_FOUND = 'token_not_found';
23
+ export const TOKEN_LIMIT_EXCEEDED = 'token_limit_exceeded';
24
+ export const FILE_PATH_MISSING = 'file_path_missing';
25
+ export const FILE_READ_ERROR = 'file_read_error';
26
+ export const SOURCE_MISSING = 'source_missing';
27
+ export const DBML_PARSE_ERROR = 'dbml_parse_error';
28
+ export const CONFIRMATION_REQUIRED = 'confirmation_required';
29
+ export const VALIDATION_FAILED = 'validation_failed';
30
+ export const INTERNAL_SERVER_ERROR = 'internal_server_error';
31
+ export const NETWORK_ERROR = 'network_error';
32
+ export const UNEXPECTED_ERROR = 'unexpected_error';
33
+ export const REAUTH_MESSAGE = [
34
+ 'Authentication failed: your credentials are invalid or expired.',
35
+ ' - If you set DBDIAGRAM_TOKEN, replace it with a valid token'
36
+ + ' (it takes precedence, so unset it to use `dbdiagram auth login` instead).',
37
+ ' - Otherwise, run `dbdiagram auth login` to re-authenticate.',
38
+ ].join('\n');
39
+ export const NOT_LOGGED_IN_MESSAGE = 'Not logged in. Run `dbdiagram auth login`, or set DBDIAGRAM_TOKEN, to authenticate.';
40
+ export const CLI_ERROR_MESSAGES = {
41
+ [AUTH_NOT_LOGGED_IN]: NOT_LOGGED_IN_MESSAGE,
42
+ [AUTH_INVALID_CREDENTIALS]: REAUTH_MESSAGE,
43
+ [AUTH_LOGIN_TIMEOUT]: 'Login timed out. Please try again.',
44
+ [AUTH_LOGIN_CANCELLED]: 'Login cancelled.',
45
+ [AUTH_LOGIN_STATE_MISMATCH]: 'Authentication failed: security validation error. Please try again.',
46
+ [AUTH_LOGIN_INVALID_CALLBACK]: 'Authentication failed: invalid callback received. Please try again.',
47
+ [AUTH_LOGIN_EXCHANGE_FAILED]: 'Authentication failed: could not exchange token. Please try again.',
48
+ [AUTH_LOGIN_SERVER_ERROR]: 'Authentication failed: could not start local server. '
49
+ + 'Another process may be using the required port. Please try again.',
50
+ [AUTH_LOGIN_FAILED]: 'Authentication failed: an unexpected error occurred.',
51
+ [DIAGRAM_ID_MISSING]: 'Missing required flag: --diagram-id',
52
+ [DIAGRAM_NOT_FOUND]: 'Diagram not found.',
53
+ [DIAGRAM_PERMISSION_DENIED]: 'You do not have permission to perform this action on this diagram.',
54
+ [DIAGRAM_LIMIT_EXCEEDED]: 'Diagram limit exceeded for your current plan.',
55
+ [WORKSPACE_NAME_MISSING]: 'Workspace url name is required. Use --project <workspace>/<project> '
56
+ + 'or set workspaceUrlName in settings.json',
57
+ [WORKSPACE_NOT_FOUND]: 'Workspace not found.',
58
+ [WORKSPACE_PERMISSION_DENIED]: 'You do not have permission to access this workspace.',
59
+ [PROJECT_NAME_MISSING]: 'Project name/url name is required. Use --project <workspace>/<project> '
60
+ + 'or set projectUrlName in settings.json',
61
+ [TOKEN_NOT_FOUND]: 'Token not found.',
62
+ [TOKEN_LIMIT_EXCEEDED]: 'Token limit reached. Delete an existing token before creating a new one.',
63
+ [FILE_PATH_MISSING]: 'Missing required argument: filepath. Provide it as an argument or set dbml.entry in settings.json.',
64
+ [SOURCE_MISSING]: 'One source must be specified: --from-file or --from-diagram',
65
+ [CONFIRMATION_REQUIRED]: 'Confirmation required. Pass --force (-f) to delete in a non-interactive environment.',
66
+ [INTERNAL_SERVER_ERROR]: 'An internal server error occurred. Please try again later.',
67
+ [NETWORK_ERROR]: 'Network error: could not reach the server. Check your connection and try again.',
68
+ };
69
+ export const PORTAL_TO_CLI_CODE = {
70
+ diagram_not_found: DIAGRAM_NOT_FOUND,
71
+ diagram_permission_denied_view: DIAGRAM_PERMISSION_DENIED,
72
+ diagram_permission_denied_edit: DIAGRAM_PERMISSION_DENIED,
73
+ diagram_permission_denied_delete: DIAGRAM_PERMISSION_DENIED,
74
+ diagram_limit_exceeded: DIAGRAM_LIMIT_EXCEEDED,
75
+ workspace_not_found: WORKSPACE_NOT_FOUND,
76
+ workspace_permission_denied_view: WORKSPACE_PERMISSION_DENIED,
77
+ token_not_found: TOKEN_NOT_FOUND,
78
+ token_limit_exceeded: TOKEN_LIMIT_EXCEEDED,
79
+ internal_server_error: INTERNAL_SERVER_ERROR,
80
+ unexpected_request_error: NETWORK_ERROR,
81
+ };
@@ -7,3 +7,4 @@ export const DIAGRAM_VIZ_FILE_VERSIONS = {
7
7
  v1: '1.0.0',
8
8
  v2: '2.0.0',
9
9
  };
10
+ export const TELEMETRY_FILE_NAME = 'telemetry.json';
@@ -0,0 +1,9 @@
1
+ export class CliError extends Error {
2
+ errorCode;
3
+ constructor(errorCode, message) {
4
+ super(message);
5
+ this.errorCode = errorCode;
6
+ this.name = 'CliError';
7
+ Error.captureStackTrace?.(this, CliError);
8
+ }
9
+ }
@@ -15,6 +15,7 @@ export class PortalApiError extends Error {
15
15
  this.data = data;
16
16
  this.type = type;
17
17
  this.name = 'PortalApiError';
18
+ Error.captureStackTrace?.(this, PortalApiError);
18
19
  }
19
20
  static fromAxios(error) {
20
21
  if (error.response) {
@@ -1,6 +1,13 @@
1
1
  import { resolveCredential } from '../config/credential-manager.js';
2
2
  import { resolveSettings } from '../config/settings-manager.js';
3
- export function globalPreAction(_thisCommand, _actionCommand) {
3
+ import { flushTelemetry, initTelemetry, trackEvent } from '../services/telemetry.service.js';
4
+ import { resolveCommandName } from '../utils/command-name.util.js';
5
+ export function globalPreAction(_thisCommand, actionCommand) {
4
6
  resolveCredential();
5
7
  resolveSettings();
8
+ initTelemetry();
9
+ trackEvent({ name: 'cli_command_run', properties: { command: resolveCommandName(actionCommand) } });
10
+ }
11
+ export function globalPostAction() {
12
+ flushTelemetry();
6
13
  }
package/dist/index.js CHANGED
@@ -1,12 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  import 'dotenv/config';
3
+ import { CommanderError } from 'commander';
3
4
  import { load } from './commands/command.loader.js';
4
5
  import { createProgram } from './program.js';
5
- import { globalPreAction } from './hooks/global.hook.js';
6
+ import { globalPostAction, globalPreAction } from './hooks/global.hook.js';
7
+ import { flushTelemetry, initTelemetry, trackEvent } from './services/telemetry.service.js';
8
+ const getAttemptedCommand = () => {
9
+ const operands = process.argv.slice(2).filter((arg) => !arg.startsWith('-'));
10
+ return operands.length ? operands.join(' ') : 'unknown';
11
+ };
6
12
  const bootstrap = async () => {
7
13
  const program = createProgram();
8
14
  load(program);
9
15
  program.hook('preAction', globalPreAction);
10
- await program.parseAsync(process.argv);
16
+ program.hook('postAction', globalPostAction);
17
+ try {
18
+ await program.parseAsync(process.argv);
19
+ }
20
+ catch (error) {
21
+ if (!(error instanceof CommanderError))
22
+ throw error;
23
+ if (error.code === 'commander.unknownCommand' ||
24
+ error.code === 'commander.excessArguments') {
25
+ initTelemetry();
26
+ trackEvent({ name: 'cli_command_run', properties: { command: getAttemptedCommand() } });
27
+ flushTelemetry();
28
+ }
29
+ process.exitCode = error.exitCode;
30
+ }
11
31
  };
12
32
  bootstrap();
@@ -0,0 +1,77 @@
1
+ import { buildSelfDescribingEvent, newTracker } from '@snowplow/node-tracker';
2
+ import { TELEMETRY_CONFIGS } from '../../config.js';
3
+ export const constructSnowplowSchemaUrl = (name, version = '1-0-0') => `iglu:io.dbdiagram/${name}/jsonschema/${version}`;
4
+ let tracker;
5
+ export const getTracker = () => {
6
+ if (tracker)
7
+ return tracker;
8
+ tracker = newTracker({
9
+ namespace: 'cli',
10
+ appId: TELEMETRY_CONFIGS.appId,
11
+ encodeBase64: true,
12
+ }, {
13
+ endpoint: TELEMETRY_CONFIGS.collectorUrl,
14
+ eventMethod: 'post',
15
+ bufferSize: 1,
16
+ retryFailedRequests: false,
17
+ onRequestFailure: () => { },
18
+ onRequestSuccess() { },
19
+ });
20
+ return tracker;
21
+ };
22
+ export const toEventData = (event) => {
23
+ const { name, properties } = event;
24
+ switch (name) {
25
+ case 'cli_command_run':
26
+ return { command: properties.command };
27
+ case 'cli_diagram_push': {
28
+ const { action, diagramId, workspaceId } = properties;
29
+ return {
30
+ action,
31
+ diagram_id: diagramId,
32
+ workspace_id: workspaceId,
33
+ };
34
+ }
35
+ case 'cli_document_build': {
36
+ const { workspaceUrlName, projectUrlName } = properties;
37
+ return {
38
+ workspace_url_name: workspaceUrlName,
39
+ project_url_name: projectUrlName,
40
+ };
41
+ }
42
+ }
43
+ };
44
+ export const toClientContextEntity = (client) => ({
45
+ schema: constructSnowplowSchemaUrl('cli_client_context'),
46
+ data: {
47
+ device_id: client.deviceId,
48
+ invocation_id: client.invocationId,
49
+ cli_version: client.cliVersion,
50
+ node_version: client.nodeVersion,
51
+ os: client.os,
52
+ arch: client.arch,
53
+ actor: client.actor,
54
+ ...(client.agentName ? { agent_name: client.agentName } : {}),
55
+ environment: client.environment,
56
+ },
57
+ });
58
+ export const toUserContextEntity = (userId) => ({
59
+ schema: constructSnowplowSchemaUrl('cli_user_context'),
60
+ data: { user_id: userId },
61
+ });
62
+ export const send = (events, client) => {
63
+ const tracker = getTracker();
64
+ const clientContextEntity = toClientContextEntity(client);
65
+ for (const event of events) {
66
+ const contexts = [clientContextEntity];
67
+ if ('userId' in event) {
68
+ contexts.push(toUserContextEntity(event.userId));
69
+ }
70
+ tracker.track(buildSelfDescribingEvent({
71
+ event: { schema: constructSnowplowSchemaUrl(event.name), data: toEventData(event) },
72
+ }), contexts, { type: 'ttm', value: event.timestamp });
73
+ }
74
+ };
75
+ export const telemetryIntegration = {
76
+ send,
77
+ };