Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/whole-teeth-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'powersync': patch
---

Improved formatting for PowerSync management service call errors.
21 changes: 15 additions & 6 deletions packages/cli-core/src/command-types/PowerSyncCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { JourneyError } from '@journeyapps-labs/micro-errors';
import { Command, ux } from '@oclif/core';
import { join } from 'node:path';

import {
formatPowersyncServiceErrorDisplay,
isPowersyncAuthServiceError
} from '../utils/format-powersync-service-error-display.js';
import { CommandHelpGroup } from './HelpGroup.js';

export type StyledErrorParams = {
Expand Down Expand Up @@ -35,20 +39,25 @@ export abstract class PowerSyncCommand extends Command {
styledError(params: StyledErrorParams): never {
const { error, exitCode = 1, message, suggestions } = params;
// Journey SDK errors contain additional fields that we want to pass to the error handler.
const journeyError =
const serviceError =
error != null && typeof error === 'object' && 'is_journey_error' in error ? (error as JourneyError) : undefined;
const journeyErrorMessage = journeyError ? JSON.stringify(journeyError.toJSON(), null, '\t') : undefined;
const serviceErrorDetails = serviceError ? formatPowersyncServiceErrorDisplay(serviceError) : undefined;

const errorDetails =
journeyErrorMessage ?? (error == null ? '' : error instanceof Error ? error.message : String(error));
serviceErrorDetails ?? (error == null ? '' : error instanceof Error ? error.message : String(error));
const displayMessage = errorDetails ? `${message}, :: ${errorDetails}` : message;

const authSuggestions =
serviceError && isPowersyncAuthServiceError(serviceError) && !suggestions?.length
? ['Run `powersync login` to refresh your credentials.']
: suggestions;

this.error(ux.colorize('red', displayMessage), {
...(error instanceof Error ? error : {}),
...(journeyError?.errorData?.code && { code: journeyError.errorData.code }),
...(serviceError?.errorData?.code && { code: serviceError.errorData.code }),
exit: exitCode,
message: journeyErrorMessage ?? message,
...(suggestions?.length && { suggestions })
message: serviceErrorDetails ?? message,
...(authSuggestions?.length && { suggestions: authSuggestions })
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { JourneyError } from '@journeyapps-labs/micro-errors';

const AUTH_HINT = 'Authentication failed. Your PAT TOKEN might be invalid — run `powersync login` to refresh.';

/** HTTP 401-style and explicit authorization failures from microservice clients. */
export function isPowersyncAuthServiceError(err: JourneyError): boolean {
const { code, status } = err.errorData;
return status === 401 || code === 'AUTHORIZATION' || err.name === 'AuthorizationError';
}

function displayNameFor(err: JourneyError): string {
if (isPowersyncAuthServiceError(err)) {
return 'PowerSyncAuthError';
}

const n = err.errorData.name ?? err.name;
return n === 'JourneyError' ? 'PowerSyncError' : (n ?? 'PowerSyncError');
}

/**
* Serializes microservice {@link JourneyError} for CLI output with PowerSync branding
* and extra context for authentication failures.
*/
export function formatPowersyncServiceErrorDisplay(err: JourneyError): string {
const base = { ...(err.toJSON() as Record<string, unknown>) };
base.name = displayNameFor(err);
if (isPowersyncAuthServiceError(err)) {
base.category = 'authentication';
base.hint = AUTH_HINT;
}

return JSON.stringify(base, null, '\t');
}
Loading