diff --git a/cli/src/api/validations/cloud-validations.ts b/cli/src/api/validations/cloud-validations.ts index 0abc7d2..e98eb47 100644 --- a/cli/src/api/validations/cloud-validations.ts +++ b/cli/src/api/validations/cloud-validations.ts @@ -5,7 +5,7 @@ import { routes } from '@powersync/management-types'; import { testCloudConnections } from '../cloud/test-connection.js'; import { mergeValidationTestRunResults } from './validation-utils.js'; -import { runConfigTest, runSyncConfigTestCloud } from './validations.js'; +import { runConfigTest, runSyncConfigTest } from './validations.js'; import { ValidationTest, ValidationTestDefinition } from './ValidationTestDefinition.js'; export type RunCloudValidationsOptions = { @@ -139,7 +139,7 @@ export function getCloudValidations({ }; } - return runSyncConfigTestCloud(project); + return runSyncConfigTest(project); } } ]; diff --git a/cli/src/api/validations/self-hosted-validations.ts b/cli/src/api/validations/self-hosted-validations.ts index 67084f5..51f9629 100644 --- a/cli/src/api/validations/self-hosted-validations.ts +++ b/cli/src/api/validations/self-hosted-validations.ts @@ -1,6 +1,6 @@ import { SelfHostedProject } from '@powersync/cli-core'; -import { runConfigTest, runSyncConfigTestSelfHosted } from './validations.js'; +import { runConfigTest, runSyncConfigTest } from './validations.js'; import { ValidationTest, ValidationTestDefinition } from './ValidationTestDefinition.js'; export type RunSelfHostedValidationsOptions = { @@ -26,7 +26,7 @@ export function getSelfHostedValidationTests({ { name: ValidationTest['SYNC-CONFIG'], async run() { - return runSyncConfigTestSelfHosted(project); + return runSyncConfigTest(project); } } ] satisfies ValidationTestDefinition[]; diff --git a/cli/src/api/validations/validation-utils.ts b/cli/src/api/validations/validation-utils.ts index 52cc589..d8ee18a 100644 --- a/cli/src/api/validations/validation-utils.ts +++ b/cli/src/api/validations/validation-utils.ts @@ -1,5 +1,5 @@ import { ux } from '@oclif/core'; -import { SyncDiagnostic, ValidationResult, ValidationTestResult, ValidationTestRunResult } from '@powersync/cli-core'; +import { ValidationResult, ValidationTestResult, ValidationTestRunResult } from '@powersync/cli-core'; import { Document } from 'yaml'; import { ValidationTest } from './ValidationTestDefinition.js'; @@ -25,24 +25,22 @@ export const STABLE_OUTPUT_NAMES: Record = { /** * Merges two or more `ValidationTestRunResult` objects into one. * The merged result passes only if all inputs passed. - * Errors, warnings, and prettyOutput from all inputs are combined. + * Errors and warnings from all inputs are combined so callers can compose validation phases. */ export function mergeValidationTestRunResults(...results: ValidationTestRunResult[]): ValidationTestRunResult { const errors = results.flatMap((r) => r.errors ?? []); const warnings = results.flatMap((r) => r.warnings ?? []); - const prettyParts = results.map((r) => r.prettyOutput).filter(Boolean); return { errors: errors.length > 0 ? errors : undefined, passed: results.every((r) => r.passed), - prettyOutput: prettyParts.length > 0 ? prettyParts.join('\n') : undefined, warnings: warnings.length > 0 ? warnings : undefined }; } /** * Formats spinner text showing per-test progress while tests are running. - * These logs are indeted with bullets for readability, and update in-place as each test settles to show pass/fail status. + * These logs are indented with bullets for readability, and update in-place as each test settles to show pass/fail status. */ export function formatOraMessage( tests: ValidationTest[], @@ -64,16 +62,9 @@ export function formatValidationErrorHuman(error: unknown): string { return ux.colorize('red', `${INDENT}${BULLET} ${error}`); } -/** - * Formats one test result into human-readable plain text. - */ function formatTestResultHuman(test: ValidationTestResult): string { const status = test.passed ? '✓' : '✗'; const name = `${status} ${STABLE_OUTPUT_NAMES[test.name as ValidationTest] ?? test.name}`; - if (test.prettyOutput) { - // Use custom pretty output if provided. - return `${name}\n${test.prettyOutput}`; - } const warningLines = (test.warnings ?? []).map( (warning) => `${INDENT}${BULLET} ${ux.colorize('yellow', '[warning]')} ${warning}` @@ -107,80 +98,3 @@ export function formatValidationJson(result: ValidationResult): string { export function formatValidationYaml(result: ValidationResult): string { return new Document(result).toString(); } - -/** - * Builds a two-line diagnostic message containing a source fragment and location-prefixed message. - */ -export function formatSyncDiagnosticMessage(diagnostic: SyncDiagnostic, syncRulesContent: string): string { - const lineText = getLineAt(syncRulesContent, diagnostic.startLine); - const fragment = getLineFragment(lineText, diagnostic.startColumn); - - return `${fragment}\n${diagnostic.startLine}:${diagnostic.startColumn} ${diagnostic.message}`; -} - -/** - * Retrieves a specific 1-based line from text content. - */ -function getLineAt(content: string, lineNumber: number): string { - if (!content) { - return ''; - } - - const lines = content.split(/\r?\n/); - return lines[Math.max(0, lineNumber - 1)] ?? ''; -} - -/** - * Extracts a nearby fragment around `startColumn`, clipping to a fixed width for readability. - */ -function getLineFragment(lineText: string, startColumn: number): string { - if (!lineText) { - return '(line unavailable)'; - } - - const maxWidth = 120; - if (lineText.length <= maxWidth) { - return lineText; - } - - const centerIndex = Math.max(0, startColumn - 1); - let start = Math.max(0, centerIndex - Math.floor(maxWidth / 2)); - const end = Math.min(lineText.length, start + maxWidth); - - if (end - start < maxWidth) { - start = Math.max(0, end - maxWidth); - } - - const prefix = start > 0 ? '…' : ''; - const suffix = end < lineText.length ? '…' : ''; - - return `${prefix}${lineText.slice(start, end)}${suffix}`; -} - -/** - * Renders a sync diagnostic into two lines for human output: - * 1) gray source fragment - * 2) colored `[error]` or `[warning]` label with `line:column` prefix followed by plain message text - */ -export function renderDiagnosticForHumanOutput(diagnostic: string, level: 'error' | 'warning'): string[] { - const [fragmentRaw, locationAndMessageRaw] = diagnostic.split('\n', 2); - const fragment = fragmentRaw ?? ''; - const locationAndMessage = locationAndMessageRaw ?? ''; - - const parsed = locationAndMessage.match(/^(\d+:\d+)\s+([\s\S]+)$/); - const location = parsed?.[1] ?? ''; - const message = parsed?.[2] ?? locationAndMessage; - - const color = level === 'error' ? 'red' : 'yellow'; - const label = level === 'error' ? '[error]' : '[warning]'; - - const lines = [ux.colorize('gray', `${INDENT}${BULLET} ${fragment}`)]; - - if (location) { - lines.push(`${INDENT}${ux.colorize(color, label)} ${ux.colorize(color, location)} ${message}`); - } else { - lines.push(`${INDENT}${ux.colorize(color, label)} ${message}`); - } - - return lines; -} diff --git a/cli/src/api/validations/validations.ts b/cli/src/api/validations/validations.ts index f30f0ee..f46ab69 100644 --- a/cli/src/api/validations/validations.ts +++ b/cli/src/api/validations/validations.ts @@ -5,9 +5,9 @@ import { SERVICE_FILENAME, SYNC_FILENAME, SyncValidation, + SyncValidationError, SyncValidationTestRunResult, - validateCloudSyncRules, - validateSelfHostedSyncRules, + validateProjectSyncConfig, ValidationTestRunResult } from '@powersync/cli-core'; import { @@ -19,8 +19,6 @@ import { import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; -import { formatSyncDiagnosticMessage, renderDiagnosticForHumanOutput } from './validation-utils.js'; - /** * Validates `service.yaml` against the cloud or self-hosted schema, depending on project type. */ @@ -43,72 +41,58 @@ export async function runConfigTest(projectDir: string, isCloud: boolean): Promi } /** - * Wraps the sync validation with enhanced error and warning information. - * - Adds line numbers to locations of sync config errors. - * - Adds a pretty human-readable output string with color formatting for terminal display. + * Formats sync validation messages for CLI output. + * Core keeps `enrichedMessage` free of location prefixes so the editor can render location separately. */ -function wrapsSyncValidation(params: { result: SyncValidation; syncText: string }): SyncValidationTestRunResult { - const { result, syncText } = params; - const errors = result.diagnostics - .filter((diagnostic) => diagnostic.level === 'fatal') - .map((diagnostic) => formatSyncDiagnosticMessage(diagnostic, syncText)); - const warnings = result.diagnostics - .filter((diagnostic) => diagnostic.level === 'warning') - .map((diagnostic) => formatSyncDiagnosticMessage(diagnostic, syncText)); +function formatSyncValidationErrorForCli(error: SyncValidationError): string { + if (!error.syncConfigLocation) { + return error.enrichedMessage; + } + + const { column, line } = error.syncConfigLocation.start; + return `[Line ${line}, Column ${column}]: ${error.enrichedMessage}`; +} - const prettyOutput = [ - ...errors.map((line) => renderDiagnosticForHumanOutput(line, 'error').join('\n')), - ...warnings.map((line) => renderDiagnosticForHumanOutput(line, 'warning').join('\n')) - ].join('\n'); +/** + * Wraps the sync validation with warning and error information for terminal display. + */ +function wrapsSyncValidation(result: SyncValidation): SyncValidationTestRunResult { + const errors = result.errors + .filter((issue) => issue.level === 'fatal') + .map((issue) => formatSyncValidationErrorForCli(issue)); + const warnings = result.errors + .filter((issue) => issue.level === 'warning') + .map((issue) => formatSyncValidationErrorForCli(issue)); return { - diagnostics: result.diagnostics, + // Keep JSON/YAML output backward-compatible: these fields are optional and + // were historically omitted rather than emitted as empty arrays. errors: errors.length > 0 ? errors : undefined, passed: errors.length === 0, - prettyOutput, warnings: warnings.length > 0 ? warnings : undefined }; } /** - * Runs cloud sync-rules validation and maps diagnostics into warning/error message arrays. + * Runs sync-config validation and maps warnings/errors into message arrays. */ -export async function runSyncConfigTestCloud(project: CloudProject): Promise { - const syncRulesPath = join(project.projectDirectory, SYNC_FILENAME); - const syncRulesContent = - project.syncRulesContent ?? (existsSync(syncRulesPath) ? readFileSync(syncRulesPath, 'utf8') : undefined); - const syncText = syncRulesContent ?? ''; +export async function runSyncConfigTest( + project: CloudProject | SelfHostedProject +): Promise { + const syncConfigPath = join(project.projectDirectory, SYNC_FILENAME); + const syncConfigContent = + project.syncRulesContent ?? (existsSync(syncConfigPath) ? readFileSync(syncConfigPath, 'utf8') : undefined); try { - return wrapsSyncValidation({ - result: await validateCloudSyncRules({ - linked: project.linked, - syncRulesContent: syncText - }), - syncText - }); + return wrapsSyncValidation( + await validateProjectSyncConfig({ + linkedProject: project, + syncConfigContent: syncConfigContent ?? '' + }) + ); } catch (error) { const message = error instanceof Error ? error.message : String(error); - return { diagnostics: [], errors: [message], passed: false }; - } -} - -/** - * Runs self-hosted sync-rules validation and maps diagnostics into warning/error message arrays. - */ -export async function runSyncConfigTestSelfHosted(project: SelfHostedProject): Promise { - const syncRulesContent = project.syncRulesContent ?? ''; - try { - return wrapsSyncValidation({ - result: await validateSelfHostedSyncRules({ - linked: project.linked, - syncRulesContent - }), - syncText: syncRulesContent - }); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { diagnostics: [], errors: [message], passed: false }; + return { errors: [message], passed: false }; } } diff --git a/cli/src/commands/deploy/sync-config.ts b/cli/src/commands/deploy/sync-config.ts index de1cbb1..06078d9 100644 --- a/cli/src/commands/deploy/sync-config.ts +++ b/cli/src/commands/deploy/sync-config.ts @@ -1,5 +1,6 @@ import { ux } from '@oclif/core/ux'; import { WithSyncConfigFilePath } from '@powersync/cli-core'; +import { ServiceCloudConfigDecoded } from '@powersync/cli-schemas'; import { routes } from '@powersync/management-types'; import { ObjectId } from 'bson'; @@ -101,7 +102,7 @@ export default class DeploySyncConfig extends WithSyncConfigFilePath(BaseDeployC _type: linked.type, name: cloudConfigState.name, ...cloudConfigState.config - }; + } as ServiceCloudConfigDecoded; // Validate sync config const instanceStatus = await this.client diff --git a/cli/test/commands/validate.test.ts b/cli/test/commands/validate.test.ts index 123be8e..40284b1 100644 --- a/cli/test/commands/validate.test.ts +++ b/cli/test/commands/validate.test.ts @@ -13,8 +13,7 @@ import { managementClientMock, MOCK_CLOUD_IDS, resetManagementClientMocks } from const emptySyncValidation = { connections: [] as { name?: string }[], - diagnostics: [] as cliCore.SyncDiagnostic[], - errors: [] as { level: string; message: string }[] + errors: [] as cliCore.SyncValidationError[] }; type EnvSnapshot = { @@ -52,7 +51,7 @@ describe('validate', () => { describe('self-hosted', () => { it('validates sync config from --sync-config-file-path, not default sync-config.yaml', async () => { - const spy = vi.spyOn(cliCore, 'validateSelfHostedSyncRules').mockResolvedValue(emptySyncValidation as never); + const spy = vi.spyOn(cliCore, 'validateProjectSyncConfig').mockResolvedValue(emptySyncValidation as never); const projectDir = join(tmpRoot, 'powersync'); mkdirSync(projectDir, { recursive: true }); @@ -96,15 +95,15 @@ describe('validate', () => { expect(spy).toHaveBeenCalledTimes(1); const call = spy.mock.calls[0]![0]; - expect(call.syncRulesContent).toContain('SELECT 1 FROM validate_custom_path_marker'); - expect(call.syncRulesContent).not.toContain('only_in_default_sync_config_yaml'); + expect(call.syncConfigContent).toContain('SELECT 1 FROM validate_custom_path_marker'); + expect(call.syncConfigContent).not.toContain('only_in_default_sync_config_yaml'); }); }); describe('cloud', () => { it('validates sync config from --sync-config-file-path when linked as cloud', async () => { resetManagementClientMocks(); - const spy = vi.spyOn(cliCore, 'validateCloudSyncRules').mockResolvedValue(emptySyncValidation as never); + const spy = vi.spyOn(cliCore, 'validateProjectSyncConfig').mockResolvedValue(emptySyncValidation as never); const { instanceId, orgId, projectId } = MOCK_CLOUD_IDS; const projectDir = join(tmpRoot, 'powersync'); @@ -166,8 +165,8 @@ describe('validate', () => { expect(spy).toHaveBeenCalledTimes(1); const call = spy.mock.calls[0]![0]; - expect(call.syncRulesContent).toContain('SELECT 1 FROM cloud_validate_override'); - expect(call.syncRulesContent).not.toContain('cloud_default_file_only'); + expect(call.syncConfigContent).toContain('SELECT 1 FROM cloud_validate_override'); + expect(call.syncConfigContent).not.toContain('cloud_default_file_only'); }); }); }); diff --git a/examples/self-hosted/local-postgres/powersync/service.yaml b/examples/self-hosted/local-postgres/powersync/service.yaml index 16888b5..6606dab 100644 --- a/examples/self-hosted/local-postgres/powersync/service.yaml +++ b/examples/self-hosted/local-postgres/powersync/service.yaml @@ -1,5 +1,3 @@ -# TODO update this with published schema -# yaml-language-server: $schema=/Users/stevenontong/Documents/platform_code/powersync/new-cli/packages/schemas/json-schema/service-config.json # yaml-language-server: $schema=https://unpkg.com/@powersync/cli-schemas@latest/json-schema/service-config.json # # PowerSync self-hosted config – example template with all schema options documented. diff --git a/packages/cli-core/src/api/validate-sync-config.ts b/packages/cli-core/src/api/validate-sync-config.ts index 75d6d81..4343a01 100644 --- a/packages/cli-core/src/api/validate-sync-config.ts +++ b/packages/cli-core/src/api/validate-sync-config.ts @@ -17,10 +17,6 @@ import { createSelfHostedClient } from '../clients/create-self-hosted-client.js' export type ValidationTestRunResult = { errors?: string[]; passed: boolean; - /** - * Output containing errors and warnings in a pretty human-readable format. - */ - prettyOutput?: string; warnings?: string[]; }; @@ -41,11 +37,10 @@ export type ValidationTestResult = ValidationTestRunResult & { }; /** - * Sync-config-specific validation result that includes structured diagnostics. + * Sync-config validation as reported by CLI commands. Detailed editor metadata stays on + * {@link SyncValidationError}; command output only needs display strings. */ -export type SyncValidationTestRunResult = ValidationTestRunResult & { - diagnostics: SyncDiagnostic[]; -}; +export type SyncValidationTestRunResult = ValidationTestRunResult; /** * Aggregate result for the full validation test suite. @@ -56,151 +51,256 @@ export type ValidationResult = { }; /** - * Diagnostics warnings linked to the position in the sync rules file (line and column) where the issue occurs. + * Position in a sync config file. */ -export type SyncDiagnostic = { - endColumn: number; - endLine: number; - level: 'fatal' | 'warning'; - message: string; - startColumn: number; - startLine: number; +export type SyncConfigPosition = { + column: number; + line: number; +}; + +/** + * Start and end location span for a sync config validation issue. + */ +export type SyncConfigLocation = { + end: SyncConfigPosition; + start: SyncConfigPosition; +}; + +/** + * Raw response returned from the validation call made to a PowerSync instance. + */ +export type SyncErrorResponse = routes.ValidateSyncRulesResponse['errors'][number]; + +/** + * Sync validation error or warning enriched with editor and display metadata. + */ +export type SyncValidationError = SyncErrorResponse & { + /** + * Original message plus source line and caret when a sync config location is available. + * CLI output adds the line/column prefix separately so editor panels can render location once. + */ + enrichedMessage: string; + /** + * Line and column range in the sync config file. This is distinct from the raw API `location`, + * which uses character offsets. + */ + syncConfigLocation?: SyncConfigLocation; }; /** * Enhanced result of sync config validation. */ -export type SyncValidation = routes.ValidateSyncRulesResponse & { - diagnostics: SyncDiagnostic[]; +export type SyncValidation = Omit & { + errors: SyncValidationError[]; }; -const EMPTY_SYNC_RULES_ERROR: routes.ValidateSyncRulesResponse = { +const EMPTY_SYNC_CONFIG_ERROR: SyncValidation = { connections: [], errors: [ { + enrichedMessage: 'No sync config content was provided.', level: 'fatal', message: 'No sync config content was provided.' } ] }; -function getErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - -function toValidationRequestError(type: 'cloud' | 'self-hosted', cause: unknown): Error { - const causeMessage = getErrorMessage(cause); - - if (type === 'cloud') { - return new Error( - `Could not validate sync rules against the cloud instance. Deploy the instance first with "powersync deploy service-config" and try again.\n${causeMessage}` - ); - } - - return new Error( - `Could not validate sync rules against the self-hosted instance. Ensure the instance is linked and running, then try again.\n${causeMessage}` - ); -} - -function offsetToLineColumn(text: string, offset?: number): { column: number; line: number } { - const safeOffset = typeof offset === 'number' ? Math.max(0, Math.min(offset, text.length)) : 0; - let line = 1; - let column = 1; - - for (let i = 0; i < safeOffset; i++) { - if (text.codePointAt(i) === 10) { - line += 1; - column = 1; - } else { - column += 1; - } - } - - return { column, line }; -} - -function mapSyncDiagnostics(response: routes.ValidateSyncRulesResponse, syncRulesContent: string): SyncDiagnostic[] { - const topLevel = response.errors.map((error) => { - const start = offsetToLineColumn(syncRulesContent, error.location?.start_offset); - const end = offsetToLineColumn(syncRulesContent, error.location?.end_offset ?? error.location?.start_offset); - return { - endColumn: end.column, - endLine: end.line, - level: error.level, - message: error.message, - startColumn: start.column, - startLine: start.line - }; - }); - - return topLevel; -} - -function toSyncValidation(response: routes.ValidateSyncRulesResponse, syncRulesContent: string): SyncValidation { - return { - ...response, - diagnostics: mapSyncDiagnostics(response, syncRulesContent) - }; -} - -export async function validateCloudSyncRules(input: { +export async function validateCloudSyncConfig(input: { linked: ResolvedCloudCLIConfig; - syncRulesContent: string; + syncConfigContent: string; }): Promise { - if (!input.syncRulesContent.trim()) { - return toSyncValidation(EMPTY_SYNC_RULES_ERROR, input.syncRulesContent); - } - try { const client = createCloudClient(); - const response = await client.validateSyncRules({ - app_id: input.linked.project_id, - id: input.linked.instance_id, - org_id: input.linked.org_id, - sync_rules: input.syncRulesContent - }); - return toSyncValidation(response, input.syncRulesContent); + return enrichSyncValidationResult({ + result: await client.validateSyncRules({ + app_id: input.linked.project_id, + id: input.linked.instance_id, + org_id: input.linked.org_id, + sync_rules: input.syncConfigContent + }), + syncConfigContent: input.syncConfigContent + }); } catch (error) { - throw toValidationRequestError('cloud', error); + return wrapSyncValidationError({ + errorCause: error, + message: `Could not validate sync config against the cloud instance. Deploy the instance first with "powersync deploy service-config" and try again.` + }); } } -export async function validateSelfHostedSyncRules(input: { +export async function validateSelfHostedSyncConfig(input: { linked: ResolvedSelfHostedCLIConfig; - syncRulesContent: string; + syncConfigContent: string; }): Promise { - if (!input.syncRulesContent.trim()) { - return toSyncValidation(EMPTY_SYNC_RULES_ERROR, input.syncRulesContent); - } - try { const client = createSelfHostedClient({ apiKey: input.linked.api_key, apiUrl: input.linked.api_url }); - const response = await client.validate({ sync_rules: input.syncRulesContent }); - - return toSyncValidation(response, input.syncRulesContent); + return enrichSyncValidationResult({ + result: await client.validate({ sync_rules: input.syncConfigContent }), + syncConfigContent: input.syncConfigContent + }); } catch (error) { - throw toValidationRequestError('self-hosted', error); + return wrapSyncValidationError({ + errorCause: error, + message: `Could not validate sync config against the self-hosted instance. Ensure the instance is linked and running, then try again.` + }); } } export async function validateProjectSyncConfig(input: { linkedProject: CloudProject | SelfHostedProject; - syncRulesContent: string; + syncConfigContent: string; }): Promise { + if (!input.syncConfigContent.trim()) { + return EMPTY_SYNC_CONFIG_ERROR; + } + if (input.linkedProject.linked.type === 'cloud') { - return validateCloudSyncRules({ + return validateCloudSyncConfig({ linked: input.linkedProject.linked, - syncRulesContent: input.syncRulesContent + syncConfigContent: input.syncConfigContent }); } - return validateSelfHostedSyncRules({ + return validateSelfHostedSyncConfig({ linked: input.linkedProject.linked, - syncRulesContent: input.syncRulesContent + syncConfigContent: input.syncConfigContent }); } + +/** + * Enriches a Sync config validation result by: + * - adding `syncConfigLocation` with line and column information when available + * - adding `enrichedMessage`, which includes source context for terminal and editor details display + * + * This keeps `message` raw for marker hover text while giving callers enough structured data + * to decide how to render line/column details. + */ +function enrichSyncValidationResult({ + result, + syncConfigContent +}: { + result: routes.ValidateSyncRulesResponse; + syncConfigContent: string; +}): SyncValidation { + return { + ...result, + errors: result.errors.map((error) => { + const syncConfigLocation = getSyncConfigLocation({ error, syncConfigContent }); + + return { + ...error, + enrichedMessage: formatSyncValidationMessage({ + message: error.message, + syncConfigContent, + syncConfigLocation + }), + ...(syncConfigLocation ? { syncConfigLocation } : {}) + }; + }) + }; +} + +function getSyncConfigLocation({ + error, + syncConfigContent +}: { + error: SyncErrorResponse; + syncConfigContent: string; +}): SyncConfigLocation | undefined { + if (!error.location) { + return undefined; + } + + return { + end: getLineAndColumnFromCharOffset({ + charOffset: error.location.end_offset, + text: syncConfigContent + }), + start: getLineAndColumnFromCharOffset({ + charOffset: error.location.start_offset, + text: syncConfigContent + }) + }; +} + +/** + * Wraps validation transport/setup failures into the same shape as API validation errors. + */ +function wrapSyncValidationError({ errorCause, message }: { errorCause: unknown; message: string }): SyncValidation { + const cause = errorCause instanceof Error ? errorCause.message : String(errorCause); + return { + connections: [], + errors: [ + { + enrichedMessage: `${message}. Cause: ${cause}`, + level: 'fatal', + message: `${message}. Cause: ${cause}` + } + ] + }; +} + +/** + * Maps the API's raw character offsets to one-based line/column positions for editor markers. + */ +function getLineAndColumnFromCharOffset({ + charOffset, + text +}: { + charOffset: number; + text: string; +}): SyncConfigPosition { + const lines = text.split('\n'); + let charCount = 0; + + for (const [i, line_] of lines.entries()) { + const line = line_ || ''; + const lineLengthWithNewline = line.length + 1; + + if (charCount + lineLengthWithNewline > charOffset) { + return { + column: charOffset - charCount + 1, + line: i + 1 + }; + } + + charCount += lineLengthWithNewline; + } + + const lastLine = lines.at(-1) || ''; + return { + column: lastLine.length + 1, + line: lines.length + }; +} + +/** + * Builds a multi-line validation message containing the original message, source fragment and caret. + */ +function formatSyncValidationMessage({ + message, + syncConfigContent, + syncConfigLocation +}: { + message: string; + syncConfigContent: string; + syncConfigLocation?: SyncConfigLocation; +}): string { + if (!syncConfigLocation) { + return message; + } + + const sourceLine = syncConfigContent.split('\n')[syncConfigLocation.start.line - 1]; + if (sourceLine == null) { + return message; + } + + const caretPrefix = `${' '.repeat(Math.max((syncConfigLocation.start.column ?? 1) - 1, 0))}^`; + + return `${message}\n${sourceLine}\n${caretPrefix}`; +} diff --git a/packages/editor/src/components/MonacoEditor.tsx b/packages/editor/src/components/MonacoEditor.tsx index 770a9ef..bedcceb 100644 --- a/packages/editor/src/components/MonacoEditor.tsx +++ b/packages/editor/src/components/MonacoEditor.tsx @@ -1,4 +1,4 @@ -import type { editor } from 'monaco-editor'; +import type { editor, Environment } from 'monaco-editor'; import MonacoReactEditor, { type BeforeMount, loader, type Monaco, type OnMount } from '@monaco-editor/react'; import * as monaco from 'monaco-editor'; @@ -14,8 +14,8 @@ import { YAML_SCHEMAS } from '../utils/yaml-schemas'; loader.config({ monaco }); if (typeof globalThis !== 'undefined') { - globalThis.MonacoEnvironment = { - getWorker(_, label) { + (globalThis as typeof globalThis & { MonacoEnvironment: Environment }).MonacoEnvironment = { + getWorker(_: string, label: string) { switch (label) { case 'yaml': { return new YamlWorker(); diff --git a/packages/editor/src/components/file-editors/BaseEditorWidget.tsx b/packages/editor/src/components/file-editors/BaseEditorWidget.tsx index d6f2e4c..0bf97da 100644 --- a/packages/editor/src/components/file-editors/BaseEditorWidget.tsx +++ b/packages/editor/src/components/file-editors/BaseEditorWidget.tsx @@ -6,10 +6,12 @@ import * as Monaco from 'monaco-editor'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { SaveFileRequest } from '../../utils/files/files'; +import type { ValidationError } from './ValidationError'; import { saveData as saveDataFn } from '../../utils/files/files.functions'; import { useTrackedFiles } from '../hooks/useFiles'; import { MonacoEditor } from '../MonacoEditor'; +import { ValidationDetailsPanel } from './ValidationDetailsPanel'; function toSaveFilename(filename: string): null | SaveFileRequest['filename'] { if (filename === 'service.yaml' || filename === 'sync-config.yaml') { @@ -37,6 +39,7 @@ export type ValidationHookParams = { export type ValidationHookResult = { markerOwner: string; markers: Monaco.editor.IMarker[]; + validationErrors: ValidationError[]; }; /** @@ -55,7 +58,8 @@ export type BaseEditorWidgetProps = { const useEmptyValidationHook: UseValidationHook = () => ({ markerOwner: '', - markers: [] + markers: [], + validationErrors: [] }); function getStatusBadge(status: Status, hasChanges: boolean) { @@ -106,43 +110,12 @@ function getValidationBadge(validationSummary: { errors: number; warnings: numbe }; } -function ValidationDetailsPanel({ markers, onHide }: { markers: Monaco.editor.IMarker[]; onHide: () => void }) { - return ( -
-
-
- Validation details -
- -
-
    - {markers.map((marker, idx) => { - const isError = Monaco.MarkerSeverity.Error === marker.severity; - const tone = isError - ? 'text-destructive-foreground bg-destructive/15 border-destructive/40' - : 'text-warning-foreground bg-warning/15 border-warning/40'; - const label = isError ? 'Error' : 'Warning'; - return ( -
  • - - {label} - -
    -
    Line {marker.startLineNumber}
    -
    {marker.message}
    -
    -
  • - ); - })} -
-
- ); +function toSchemaValidationError(marker: Monaco.editor.IMarker): ValidationError { + return { + level: marker.severity === Monaco.MarkerSeverity.Error ? 'fatal' : 'warning', + line: marker.startLineNumber, + message: marker.message + }; } /** @@ -160,9 +133,9 @@ export function BaseEditorWidget({ const { error, isPending, isRefetching, refetch } = upstream; const saveData = useServerFn(saveDataFn); const file = useMemo(() => trackedFilesState[filename], [trackedFilesState, filename]); - const [schemaMarkers, setSchemaMarkers] = useState([]); const editorRef = useRef(null); const monacoRef = useRef(null); + const [schemaValidationErrors, setSchemaValidationErrors] = useState([]); const validation = useValidationHook({ content: file?.content ?? null, editorRef, @@ -170,16 +143,17 @@ export function BaseEditorWidget({ monacoRef }); - const validationMarkers = useMemo( - () => [...schemaMarkers, ...validation.markers], - [schemaMarkers, validation.markers] + const { markerOwner, validationErrors: customValidationErrors } = validation; + const validationErrors = useMemo( + () => [...schemaValidationErrors, ...customValidationErrors], + [customValidationErrors, schemaValidationErrors] ); const validationSummary = useMemo(() => { - const errors = validationMarkers.filter((m) => Monaco.MarkerSeverity.Error === m.severity).length; - const warnings = validationMarkers.filter((m) => Monaco.MarkerSeverity.Warning === m.severity).length; + const errors = validationErrors.filter((detail) => detail.level === 'fatal').length; + const warnings = validationErrors.filter((detail) => detail.level === 'warning').length; return { errors, warnings }; - }, [validationMarkers]); + }, [validationErrors]); const hasChanges = trackedFilesState[filename]?.hasChanges ?? false; const hasValidationIssues = validationSummary.errors > 0 || validationSummary.warnings > 0; @@ -189,6 +163,20 @@ export function BaseEditorWidget({ // but disabling the editor during this window avoids confusion. const isSaveOrRefetchInProgress = status === 'saving' || isRefetching; + const handleEditorValidate = useCallback( + (markers: Monaco.editor.IMarker[]) => { + const schemaMarkers = markerOwner + ? markers.filter((marker) => { + const { owner } = marker as Monaco.editor.IMarker & { owner?: string }; + return owner !== markerOwner; + }) + : markers; + + setSchemaValidationErrors(schemaMarkers.map((marker) => toSchemaValidationError(marker))); + }, + [markerOwner] + ); + useEffect(() => { if (validationSummary.errors === 0 && validationSummary.warnings === 0) { setShowValidation(false); @@ -321,8 +309,8 @@ export function BaseEditorWidget({ )} - {showValidation && validationMarkers.length > 0 && ( - setShowValidation(false)} /> + {showValidation && validationErrors.length > 0 && ( + setShowValidation(false)} /> )}
@@ -337,19 +325,7 @@ export function BaseEditorWidget({ editorRef.current = editorInstance; monacoRef.current = monacoInstance; }} - onValidate={(markers) => { - if (!validation.markerOwner) { - setSchemaMarkers(markers); - return; - } - - const filteredMarkers = markers.filter((marker) => { - const markerOwner = (marker as Monaco.editor.IMarker & { owner?: string }).owner; - return markerOwner !== validation.markerOwner; - }); - - setSchemaMarkers(filteredMarkers); - }} + onValidate={handleEditorValidate} options={{ readOnly: isSaveOrRefetchInProgress }} path={filename} value={file.content} diff --git a/packages/editor/src/components/file-editors/SyncConfigYamlEditorWidgetProvider.tsx b/packages/editor/src/components/file-editors/SyncConfigYamlEditorWidgetProvider.tsx index 2988b77..1b90a63 100644 --- a/packages/editor/src/components/file-editors/SyncConfigYamlEditorWidgetProvider.tsx +++ b/packages/editor/src/components/file-editors/SyncConfigYamlEditorWidgetProvider.tsx @@ -1,5 +1,5 @@ import { BaseEditorWidget } from './BaseEditorWidget'; -import { useSyncRulesValidationMarkers } from './use-sync-rules-validation-markers'; +import { useSyncConfigValidationMarkers } from './use-sync-config-validation-markers'; /** * Props for the sync-config YAML editor provider. @@ -9,8 +9,10 @@ export type SyncConfigYamlEditorWidgetProviderProps = { }; /** - * Provider for editing `sync-config.yaml` with sync-rules validation enabled. + * Provider for editing `sync-config.yaml` with sync-config validation enabled. */ export function SyncConfigYamlEditorWidgetProvider({ filename }: SyncConfigYamlEditorWidgetProviderProps) { - return ; + return ( + + ); } diff --git a/packages/editor/src/components/file-editors/ValidationDetailsPanel.tsx b/packages/editor/src/components/file-editors/ValidationDetailsPanel.tsx new file mode 100644 index 0000000..a787de6 --- /dev/null +++ b/packages/editor/src/components/file-editors/ValidationDetailsPanel.tsx @@ -0,0 +1,65 @@ +import { Info } from 'lucide-react'; + +import type { ValidationError } from './ValidationError'; + +/** + * Renders a validation message while preserving optional source context and caret lines. + */ +function ValidationMarkerMessage({ message }: { message: string }) { + const [summary, ...details] = message.split('\n'); + + if (details.length === 0) { + return
{message}
; + } + + return ( +
+
{summary}
+
+        {details.join('\n')}
+      
+
+ ); +} + +/** + * Shows validation errors and warnings in the collapsible details block above the editor. + */ +export function ValidationDetailsPanel({ details, onHide }: { details: ValidationError[]; onHide: () => void }) { + return ( +
+
+
+ Validation details +
+ +
+
    + {details.map((detail, idx) => { + const isError = detail.level === 'fatal'; + const tone = isError + ? 'text-destructive-foreground bg-destructive/15 border-destructive/40' + : 'text-warning-foreground bg-warning/15 border-warning/40'; + const label = isError ? 'Error' : 'Warning'; + return ( +
  • + + {label} + +
    + {detail.line &&
    Line {detail.line}
    } + +
    +
  • + ); + })} +
+
+ ); +} diff --git a/packages/editor/src/components/file-editors/ValidationError.ts b/packages/editor/src/components/file-editors/ValidationError.ts new file mode 100644 index 0000000..de3d12c --- /dev/null +++ b/packages/editor/src/components/file-editors/ValidationError.ts @@ -0,0 +1,5 @@ +export type ValidationError = { + level: 'fatal' | 'warning'; + line?: number; + message: string; +}; diff --git a/packages/editor/src/components/file-editors/use-sync-config-validation-markers.ts b/packages/editor/src/components/file-editors/use-sync-config-validation-markers.ts new file mode 100644 index 0000000..92d8bfa --- /dev/null +++ b/packages/editor/src/components/file-editors/use-sync-config-validation-markers.ts @@ -0,0 +1,124 @@ +import type { SyncConfigLocation, SyncValidationError } from '@powersync/cli-core'; +import type { editor } from 'monaco-editor'; + +import { useServerFn } from '@tanstack/react-start'; +import { useEffect, useRef, useState } from 'react'; + +import type { UseValidationHook } from './BaseEditorWidget'; +import type { ValidationError } from './ValidationError'; + +import { validateSyncConfig as validateSyncConfigFn } from '../../utils/files/files.functions'; + +const SYNC_CONFIG_MARKER_OWNER = 'powersync-sync-config-validation'; +const VALIDATION_DEBOUNCE_MS = 350; + +// Only errors with sync-config line/column metadata can become Monaco markers. +// The full error list still feeds the Validation details panel below. +function hasSyncConfigLocation( + issue: SyncValidationError +): issue is SyncValidationError & { syncConfigLocation: SyncConfigLocation } { + return Boolean(issue.syncConfigLocation); +} + +/** + * Validation hook that runs sync-config validation and emits Monaco markers. + */ +export const useSyncConfigValidationMarkers: UseValidationHook = ({ content, editorRef, monacoRef }) => { + const validateSyncConfig = useServerFn(validateSyncConfigFn); + const [validationErrors, setValidationErrors] = useState([]); + const [markers, setMarkers] = useState([]); + const validationRunIdRef = useRef(0); + const debounceTimerRef = useRef>(null); + + useEffect(() => { + if (!content) { + setValidationErrors([]); + setMarkers([]); + const model = editorRef.current?.getModel(); + if (model && monacoRef.current) { + monacoRef.current.editor.setModelMarkers(model, SYNC_CONFIG_MARKER_OWNER, []); + } + + return; + } + + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + + debounceTimerRef.current = setTimeout(async () => { + const currentRunId = ++validationRunIdRef.current; + + try { + const result = await validateSyncConfig({ data: { content } }); + if (currentRunId !== validationRunIdRef.current) { + return; + } + + const nextMarkers: editor.IMarkerData[] = result.errors + .filter((issue) => hasSyncConfigLocation(issue)) + .map((issue) => ({ + endColumn: issue.syncConfigLocation.end.column, + endLineNumber: issue.syncConfigLocation.end.line, + message: issue.message, + severity: issue.level === 'fatal' ? 8 : 4, + source: 'powersync validate', + startColumn: issue.syncConfigLocation.start.column, + startLineNumber: issue.syncConfigLocation.start.line + })); + setValidationErrors( + result.errors.map((error) => ({ + level: error.level, + message: error.enrichedMessage, + ...(error.syncConfigLocation ? { line: error.syncConfigLocation.start.line } : {}) + })) + ); + setMarkers(nextMarkers as editor.IMarker[]); + + const model = editorRef.current?.getModel(); + if (model && monacoRef.current) { + monacoRef.current.editor.setModelMarkers(model, SYNC_CONFIG_MARKER_OWNER, nextMarkers); + } + } catch (error) { + if (currentRunId !== validationRunIdRef.current) { + return; + } + + const fallbackMarker: editor.IMarkerData = { + endColumn: 1, + endLineNumber: 1, + message: error instanceof Error ? error.message : 'Sync config validation failed.', + severity: 8, + source: 'validation', + startColumn: 1, + startLineNumber: 1 + }; + const fallbackDetail: ValidationError = { + level: 'fatal', + line: 1, + message: fallbackMarker.message + }; + + const model = editorRef.current?.getModel(); + if (model && monacoRef.current) { + monacoRef.current.editor.setModelMarkers(model, SYNC_CONFIG_MARKER_OWNER, [fallbackMarker]); + } + + setValidationErrors([fallbackDetail]); + setMarkers([fallbackMarker as editor.IMarker]); + } + }, VALIDATION_DEBOUNCE_MS); + + return () => { + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + }; + }, [content, editorRef, monacoRef, validateSyncConfig]); + + return { + markerOwner: SYNC_CONFIG_MARKER_OWNER, + markers, + validationErrors + }; +}; diff --git a/packages/editor/src/components/file-editors/use-sync-rules-validation-markers.ts b/packages/editor/src/components/file-editors/use-sync-rules-validation-markers.ts deleted file mode 100644 index 3b880dd..0000000 --- a/packages/editor/src/components/file-editors/use-sync-rules-validation-markers.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { editor } from 'monaco-editor'; - -import { useServerFn } from '@tanstack/react-start'; -import { useEffect, useRef, useState } from 'react'; - -import type { UseValidationHook } from './BaseEditorWidget'; - -import { validateSyncRules as validateSyncRulesFn } from '../../utils/files/files.functions'; - -const SYNC_RULES_MARKER_OWNER = 'powersync-sync-rules-validation'; -const VALIDATION_DEBOUNCE_MS = 350; - -/** - * Validation hook that runs sync-rules validation and emits Monaco markers. - */ -export const useSyncRulesValidationMarkers: UseValidationHook = ({ content, editorRef, monacoRef }) => { - const validateSyncRules = useServerFn(validateSyncRulesFn); - const [markers, setMarkers] = useState([]); - const validationRunIdRef = useRef(0); - const debounceTimerRef = useRef>(null); - - useEffect(() => { - if (!content) { - setMarkers([]); - const model = editorRef.current?.getModel(); - if (model && monacoRef.current) { - monacoRef.current.editor.setModelMarkers(model, SYNC_RULES_MARKER_OWNER, []); - } - - return; - } - - if (debounceTimerRef.current) { - clearTimeout(debounceTimerRef.current); - } - - debounceTimerRef.current = setTimeout(async () => { - const currentRunId = ++validationRunIdRef.current; - - try { - const result = await validateSyncRules({ data: { content } }); - if (currentRunId !== validationRunIdRef.current) { - return; - } - - const nextMarkers: editor.IMarkerData[] = result.issues.map((issue) => ({ - endColumn: issue.endColumn, - endLineNumber: issue.endLine, - message: issue.message, - severity: issue.level === 'fatal' ? 8 : 4, - source: 'powersync validate', - startColumn: issue.startColumn, - startLineNumber: issue.startLine - })); - - setMarkers(nextMarkers as editor.IMarker[]); - - const model = editorRef.current?.getModel(); - if (model && monacoRef.current) { - monacoRef.current.editor.setModelMarkers(model, SYNC_RULES_MARKER_OWNER, nextMarkers); - } - } catch (error) { - if (currentRunId !== validationRunIdRef.current) { - return; - } - - const fallbackMarker: editor.IMarkerData = { - endColumn: 1, - endLineNumber: 1, - message: error instanceof Error ? error.message : 'Sync rules validation failed.', - severity: 8, - source: 'validation', - startColumn: 1, - startLineNumber: 1 - }; - - const model = editorRef.current?.getModel(); - if (model && monacoRef.current) { - monacoRef.current.editor.setModelMarkers(model, SYNC_RULES_MARKER_OWNER, [fallbackMarker]); - } - - setMarkers([fallbackMarker as editor.IMarker]); - } - }, VALIDATION_DEBOUNCE_MS); - - return () => { - if (debounceTimerRef.current) { - clearTimeout(debounceTimerRef.current); - } - }; - }, [content, editorRef, monacoRef, validateSyncRules]); - - return { - markerOwner: SYNC_RULES_MARKER_OWNER, - markers - }; -}; diff --git a/packages/editor/src/utils/files/files.functions.ts b/packages/editor/src/utils/files/files.functions.ts index 3d9741a..09542f1 100644 --- a/packages/editor/src/utils/files/files.functions.ts +++ b/packages/editor/src/utils/files/files.functions.ts @@ -6,8 +6,8 @@ import fs from 'node:fs'; import path from 'node:path'; import { env } from '../../env'; -import { type FilesResponse, SaveFileRequest, ValidateSyncRulesRequest } from './files'; -import { validateSyncRulesWithCli } from './files.server'; +import { type FilesResponse, SaveFileRequest, ValidateSyncConfigRequest } from './files'; +import { validateSyncConfigWithCli } from './files.server'; // GET request (default) export const getConfigFiles = createServerFn().handler(async () => { @@ -57,13 +57,13 @@ export const saveData = createServerFn({ method: 'POST' }) }); // POST request -export const validateSyncRules = createServerFn({ method: 'POST' }) - .inputValidator(ValidateSyncRulesRequest) +export const validateSyncConfig = createServerFn({ method: 'POST' }) + .inputValidator(ValidateSyncConfigRequest) .handler(async ({ data }) => { const projectContext = env.POWERSYNC_PROJECT_CONTEXT; if (!projectContext) { throw new Error('Missing POWERSYNC_PROJECT_CONTEXT. Start the editor via "powersync edit config".'); } - return validateSyncRulesWithCli(data.content); + return validateSyncConfigWithCli(data.content); }); diff --git a/packages/editor/src/utils/files/files.server.ts b/packages/editor/src/utils/files/files.server.ts index ef95b85..e422f5d 100644 --- a/packages/editor/src/utils/files/files.server.ts +++ b/packages/editor/src/utils/files/files.server.ts @@ -1,3 +1,5 @@ +import type { SyncValidation } from '@powersync/cli-core'; + import { validateProjectSyncConfig } from '@powersync/cli-core'; import { env } from '../../env'; @@ -5,18 +7,13 @@ import { env } from '../../env'; /** * Validates the PowerSync sync config server side. */ -export async function validateSyncRulesWithCli(syncRulesContent: string) { +export async function validateSyncConfigWithCli(syncConfigContent: string): Promise { if (!env.POWERSYNC_PROJECT_CONTEXT) { throw new Error('POWERSYNC_PROJECT_CONTEXT is not set. Open the editor via the CLI.'); } - const syncTest = await validateProjectSyncConfig({ + return validateProjectSyncConfig({ linkedProject: env.POWERSYNC_PROJECT_CONTEXT.linkedProject, - syncRulesContent + syncConfigContent }); - - return { - issues: syncTest.diagnostics, - passed: syncTest.diagnostics.filter((d) => d.level === 'fatal').length === 0 - }; } diff --git a/packages/editor/src/utils/files/files.ts b/packages/editor/src/utils/files/files.ts index 81e49d9..e3321b5 100644 --- a/packages/editor/src/utils/files/files.ts +++ b/packages/editor/src/utils/files/files.ts @@ -1,4 +1,4 @@ -import type { SyncDiagnostic } from '@powersync/cli-core'; +import type { SyncValidation } from '@powersync/cli-core'; import { z } from 'zod'; @@ -30,18 +30,15 @@ export const SaveFileRequest = z.object({ export type SaveFileRequest = z.infer; /** - * Validates the sync rules content and returns any issues found. + * Validates sync config content and returns any issues found. */ -export const ValidateSyncRulesRequest = z.object({ +export const ValidateSyncConfigRequest = z.object({ content: z.string() }); -export type ValidateSyncRulesRequest = z.infer; +export type ValidateSyncConfigRequest = z.infer; /** - * Result from sync rules validation. + * Result from sync config validation. */ -export type ValidateSyncRulesResponse = { - issues: SyncDiagnostic[]; - passed: boolean; -}; +export type ValidateSyncConfigResponse = SyncValidation; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e8adf9f..14edd97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,20 +10,20 @@ catalogs: specifier: ^4.10.6 version: 4.11.4 '@powersync/management-client': - specifier: ^0.1.1 + specifier: ^0.1.3 version: 0.1.3 '@powersync/management-types': - specifier: ^0.2.0 + specifier: ^0.2.2 version: 0.2.2 '@powersync/service-client': specifier: ^0.0.6 version: 0.0.6 '@powersync/service-schema': - specifier: ^1.21.0 + specifier: ^1.22.0 version: 1.22.0 '@powersync/service-sync-rules': - specifier: ^0.36.0 - version: 0.36.0 + specifier: ^0.37.0 + version: 0.37.0 '@powersync/service-types': specifier: ^0.15.2 version: 0.15.2 @@ -112,7 +112,7 @@ importers: version: 0.2.2 '@powersync/service-sync-rules': specifier: 'catalog:' - version: 0.36.0 + version: 0.37.0 '@powersync/service-types': specifier: 'catalog:' version: 0.15.2 @@ -356,7 +356,7 @@ importers: devDependencies: '@powersync/service-sync-rules': specifier: 'catalog:' - version: 0.36.0 + version: 0.37.0 '@types/node': specifier: ^24.12.2 version: 24.12.4 @@ -1557,8 +1557,8 @@ packages: '@powersync/service-schema@1.22.0': resolution: {integrity: sha512-RyCzjNKHqnRKavHSQw7AtNGaItNKtpoJPktAsrt3zFcR0SckRk9aqXZvSYMhJvd6zKAb9B/ZybHhNcBJRjTXGg==} - '@powersync/service-sync-rules@0.36.0': - resolution: {integrity: sha512-WlV4pOK1QwsM+6Bvy5Vkq04xmb0E1FcbWgqnQcbVJpKnSGul8osRDoIC/2JSNOauwxTqNXf0Ec8n6tzD9SSWOQ==} + '@powersync/service-sync-rules@0.37.0': + resolution: {integrity: sha512-9uGwM3EPI4DZfabClth1nDikiqerlKhiG+uS/oie4yUxk6VZGua0UsgfZSGSsulBeiHomzTSQ5MiwIT2KnRzPw==} '@powersync/service-types@0.15.2': resolution: {integrity: sha512-GXQqCuTME+S1nV9CLbxtgYuAtMGFFLkDubm+DRTNGQqur8wBVzJp3haHCHFBVJ03Bhec+K7AIrB6CTii7b/HJg==} @@ -7522,7 +7522,7 @@ snapshots: '@powersync/service-schema@1.22.0': {} - '@powersync/service-sync-rules@0.36.0': + '@powersync/service-sync-rules@0.37.0': dependencies: '@powersync/service-jsonbig': 0.17.13 '@syncpoint/wkx': 0.5.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 03d1d24..5d23588 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -26,11 +26,11 @@ minimumReleaseAgeExclude: catalog: '@oclif/core': ^4.10.6 - '@powersync/management-client': ^0.1.1 - '@powersync/management-types': ^0.2.0 + '@powersync/management-client': ^0.1.3 + '@powersync/management-types': ^0.2.2 '@powersync/service-client': ^0.0.6 - '@powersync/service-schema': ^1.21.0 - '@powersync/service-sync-rules': ^0.36.0 + '@powersync/service-schema': ^1.22.0 + '@powersync/service-sync-rules': ^0.37.0 '@powersync/service-types': ^0.15.2 oclif: 4.23.0