From 548e2edfbfdde0c8c5769cbbea5adb6964c20225 Mon Sep 17 00:00:00 2001 From: Serge Pavlyuk Date: Mon, 30 Jun 2025 12:59:42 +0300 Subject: [PATCH 01/40] Add entry zod schemas --- package-lock.json | 12 +- package.json | 3 +- src/shared/sdk/BaseSdk.ts | 32 + src/shared/sdk/index.ts | 0 .../__tests__/dash-api.schema.test.ts | 989 ++++++++++++++++++ src/shared/sdk/zod-shemas/chart-api.schema.ts | 495 +++++++++ src/shared/sdk/zod-shemas/dash-api.schema.ts | 275 +++++ .../sdk/zod-shemas/dataset-api.schema.ts | 339 ++++++ 8 files changed, 2143 insertions(+), 2 deletions(-) create mode 100644 src/shared/sdk/BaseSdk.ts create mode 100644 src/shared/sdk/index.ts create mode 100644 src/shared/sdk/zod-shemas/__tests__/dash-api.schema.test.ts create mode 100644 src/shared/sdk/zod-shemas/chart-api.schema.ts create mode 100644 src/shared/sdk/zod-shemas/dash-api.schema.ts create mode 100644 src/shared/sdk/zod-shemas/dataset-api.schema.ts diff --git a/package-lock.json b/package-lock.json index 36defcd5a9..0d807fba10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,7 +72,8 @@ "request-ip": "^3.3.0", "request-promise-native": "^1.0.9", "set-cookie-parser": "^2.7.1", - "workerpool": "^9.1.1" + "workerpool": "^9.1.1", + "zod": "^3.25.64" }, "devDependencies": { "@gravity-ui/app-builder": "^0.28.0", @@ -32268,6 +32269,15 @@ "engines": { "node": ">=10" } + }, + "node_modules/zod": { + "version": "3.25.64", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.64.tgz", + "integrity": "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index d3307f5929..2e528d1eb7 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,8 @@ "request-ip": "^3.3.0", "request-promise-native": "^1.0.9", "set-cookie-parser": "^2.7.1", - "workerpool": "^9.1.1" + "workerpool": "^9.1.1", + "zod": "^3.25.64" }, "devDependencies": { "@gravity-ui/app-builder": "^0.28.0", diff --git a/src/shared/sdk/BaseSdk.ts b/src/shared/sdk/BaseSdk.ts new file mode 100644 index 0000000000..593ccf7c3f --- /dev/null +++ b/src/shared/sdk/BaseSdk.ts @@ -0,0 +1,32 @@ +export abstract class BaseSdk< + T, + M extends Record T> = {}, + S extends Record = {}, + O extends object = {}, +> { + abstract mutations?: M; + abstract selectors?: S; + + abstract create(): Promise; + abstract get(options: {id: string} & O): Promise; + abstract update(options: {id: string} & O, newEntry: T): Promise; + abstract delete(options: {id: string} & O): Promise; + + async lazyBatch(options: {id: string} & O) { + const entry = await this.get(options); + + return this.batchMutations.bind(this, entry); + } + + batchMutations(entry: T, mutations: Array<{name: keyof M; options: any}>) { + let newEntry = entry; + + for (const mutation of mutations) { + if (this.mutations && mutation.name in this.mutations) { + newEntry = this.mutations[mutation.name](entry, mutation.options); + } + } + + return newEntry; + } +} diff --git a/src/shared/sdk/index.ts b/src/shared/sdk/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/shared/sdk/zod-shemas/__tests__/dash-api.schema.test.ts b/src/shared/sdk/zod-shemas/__tests__/dash-api.schema.test.ts new file mode 100644 index 0000000000..4748c10a78 --- /dev/null +++ b/src/shared/sdk/zod-shemas/__tests__/dash-api.schema.test.ts @@ -0,0 +1,989 @@ +import { + CONTROLS_PLACEMENT_MODE, + DASH_CURRENT_SCHEME_VERSION, + DashLoadPriority, + DashTabConnectionKind, + DashTabItemControlElementType, + DashTabItemControlSourceType, + DashTabItemTitleSizes, + DashTabItemType, +} from '../../../'; +import {type DashSchema, dashSchema} from '../dash-api.schema'; + +const DASH_DEFAULT_NAMESPACE = 'default'; + +describe('dashSchema', () => { + describe('valid configurations', () => { + it('should validate minimal valid dashboard configuration', () => { + const validConfig: DashSchema = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(validConfig); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(validConfig); + } + }); + + it('should validate complete dashboard configuration with all optional fields', () => { + const validConfig: DashSchema = { + key: 'dashboard-key-123', + data: { + counter: 1, + salt: 'random-salt-123', + schemeVersion: DASH_CURRENT_SCHEME_VERSION, + tabs: [ + { + id: 'tab1', + title: 'Complete Tab', + items: [ + { + id: 'text1', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Text, + data: { + text: 'Sample text content', + }, + }, + { + id: 'title1', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Title, + data: { + text: 'Sample Title', + size: DashTabItemTitleSizes.L, + showInTOC: true, + }, + }, + { + id: 'widget1', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Widget, + data: { + hideTitle: false, + tabs: [ + { + id: 'widget-tab1', + title: 'Widget Tab', + description: 'Widget description', + chartId: 'chart123', + isDefault: true, + params: {param1: 'value1'}, + autoHeight: true, + }, + ], + }, + }, + ], + layout: [ + { + i: 'text1', + h: 2, + w: 12, + x: 0, + y: 0, + }, + { + i: 'title1', + h: 1, + w: 12, + x: 0, + y: 2, + parent: 'text1', + }, + ], + connections: [ + { + from: 'control1', + to: 'widget1', + kind: DashTabConnectionKind.Ignore, + }, + ], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [['alias1', 'alias2']], + }, + }, + ], + settings: { + autoupdateInterval: 60, + maxConcurrentRequests: 5, + loadPriority: DashLoadPriority.Charts, + silentLoading: true, + dependentSelectors: false, + globalParams: {globalParam: 'value'}, + hideTabs: false, + hideDashTitle: true, + expandTOC: false, + }, + }, + meta: {metaKey: 'metaValue'}, + links: {linkKey: 'linkValue'}, + }; + + const result = dashSchema.safeParse(validConfig); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(validConfig); + } + }); + + it('should validate dashboard with control items', () => { + const validConfig: DashSchema = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Tab with Controls', + items: [ + { + id: 'control1', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Control, + data: { + title: 'Dataset Control', + sourceType: DashTabItemControlSourceType.Dataset, + source: { + datasetId: 'dataset123', + datasetFieldId: 'field123', + elementType: DashTabItemControlElementType.Select, + required: true, + showHint: true, + showTitle: true, + defaultValue: 'default', + multiselectable: false, + }, + }, + defaults: {defaultParam: 'value'}, + }, + { + id: 'control2', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Control, + data: { + title: 'Manual Control', + sourceType: DashTabItemControlSourceType.Manual, + source: { + fieldName: 'manualField', + elementType: DashTabItemControlElementType.Date, + required: false, + showHint: false, + showTitle: true, + defaultValue: '2023-01-01', + isRange: true, + acceptableValues: { + from: '2023-01-01', + to: '2023-12-31', + }, + }, + }, + defaults: {}, + }, + { + id: 'control3', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Control, + data: { + title: 'External Control', + sourceType: DashTabItemControlSourceType.External, + source: { + chartId: 'external-chart-123', + text: 'External control text', + autoHeight: true, + }, + }, + defaults: {}, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(validConfig); + expect(result.success).toBe(true); + }); + + it('should validate dashboard with group control', () => { + const validConfig: DashSchema = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Tab with Group Control', + items: [ + { + id: 'groupControl1', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.GroupControl, + data: { + group: [ + { + id: 'groupItem1', + title: 'Group Item 1', + namespace: DASH_DEFAULT_NAMESPACE, + sourceType: DashTabItemControlSourceType.Dataset, + defaults: {param: 'value'}, + placementMode: CONTROLS_PLACEMENT_MODE.AUTO, + width: '50%', + source: { + datasetId: 'dataset123', + datasetFieldId: 'field123', + elementType: + DashTabItemControlElementType.Input, + required: true, + showHint: false, + showTitle: true, + defaultValue: 'input value', + }, + }, + ], + autoHeight: true, + buttonApply: true, + buttonReset: false, + showGroupName: true, + updateControlsOnChange: false, + }, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(validConfig); + expect(result.success).toBe(true); + }); + + it('should validate different control element types', () => { + // Test Select control + const selectConfig: DashSchema = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [ + { + id: 'control-select', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Control, + data: { + title: 'Select Control', + sourceType: DashTabItemControlSourceType.Manual, + source: { + fieldName: 'selectField', + elementType: DashTabItemControlElementType.Select, + required: false, + showHint: true, + showTitle: true, + defaultValue: ['option1', 'option2'], + multiselectable: true, + }, + }, + defaults: {}, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + // Test Date control + const dateConfig: DashSchema = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [ + { + id: 'control-date', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Control, + data: { + title: 'Date Control', + sourceType: DashTabItemControlSourceType.Manual, + source: { + fieldName: 'dateField', + elementType: DashTabItemControlElementType.Date, + required: false, + showHint: true, + showTitle: true, + defaultValue: '2023-01-01', + isRange: false, + }, + }, + defaults: {}, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + // Test Input control + const inputConfig: DashSchema = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [ + { + id: 'control-input', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Control, + data: { + title: 'Input Control', + sourceType: DashTabItemControlSourceType.Manual, + source: { + fieldName: 'inputField', + elementType: DashTabItemControlElementType.Input, + required: false, + showHint: true, + showTitle: true, + defaultValue: 'input text', + }, + }, + defaults: {}, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + // Test Checkbox control + const checkboxConfig: DashSchema = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [ + { + id: 'control-checkbox', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Control, + data: { + title: 'Checkbox Control', + sourceType: DashTabItemControlSourceType.Manual, + source: { + fieldName: 'checkboxField', + elementType: DashTabItemControlElementType.Checkbox, + required: false, + showHint: true, + showTitle: true, + defaultValue: 'true', + }, + }, + defaults: {}, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const configs = [selectConfig, dateConfig, inputConfig, checkboxConfig]; + configs.forEach((config) => { + const result = dashSchema.safeParse(config); + expect(result.success).toBe(true); + }); + }); + + it('should validate different title sizes', () => { + const titleSizes = Object.values(DashTabItemTitleSizes); + + titleSizes.forEach((size, index) => { + const config: DashSchema = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [ + { + id: `title${index}`, + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Title, + data: { + text: `Title ${size}`, + size: size, + }, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(config); + expect(result.success).toBe(true); + }); + }); + }); + + describe('invalid configurations', () => { + it('should reject configuration without required data field', () => { + const invalidConfig = { + key: 'test-key', + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'invalid_type', + expected: 'object', + message: 'Invalid input: expected object, received undefined', + path: ['data'], + }), + ); + } + }); + + it('should reject configuration without tabs', () => { + const invalidConfig = { + data: { + settings: {}, + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'invalid_type', + path: ['data', 'tabs'], + message: 'Invalid input: expected array, received undefined', + }), + ); + } + }); + + it('should reject tab with empty id', () => { + const invalidConfig = { + data: { + tabs: [ + { + id: '', + title: 'Test Tab', + items: [], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'too_small', + path: ['data', 'tabs', 0, 'id'], + }), + ); + } + }); + + it('should reject tab with empty title', () => { + const invalidConfig = { + data: { + tabs: [ + { + id: 'tab1', + title: '', + items: [], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'too_small', + path: ['data', 'tabs', 0, 'title'], + }), + ); + } + }); + + it('should reject item with invalid namespace', () => { + const invalidConfig = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [ + { + id: 'text1', + namespace: 'invalid-namespace', + type: DashTabItemType.Text, + data: { + text: 'Sample text', + }, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'invalid_value', + path: ['data', 'tabs', 0, 'items', 0, 'namespace'], + }), + ); + } + }); + + it('should reject control with invalid element type', () => { + const invalidConfig = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [ + { + id: 'control1', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Control, + data: { + title: 'Invalid Control', + sourceType: DashTabItemControlSourceType.Manual, + source: { + fieldName: 'field1', + elementType: 'invalid-type', + required: false, + showHint: false, + showTitle: true, + }, + }, + defaults: {}, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + }); + + it('should reject widget tab with empty chartId', () => { + const invalidConfig = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [ + { + id: 'widget1', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Widget, + data: { + hideTitle: false, + tabs: [ + { + id: 'widget-tab1', + title: 'Widget Tab', + chartId: '', + }, + ], + }, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'too_small', + path: ['data', 'tabs', 0, 'items', 0, 'data', 'tabs', 0, 'chartId'], + }), + ); + } + }); + + it('should reject invalid scheme version', () => { + const invalidConfig = { + data: { + schemeVersion: DASH_CURRENT_SCHEME_VERSION + 1, + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'too_big', + path: ['data', 'schemeVersion'], + }), + ); + } + }); + + it('should reject invalid autoupdate interval', () => { + const invalidConfig = { + data: { + tabs: [], + settings: { + autoupdateInterval: 15, // less than minimum 30 + }, + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'invalid_union', + path: ['data', 'settings', 'autoupdateInterval'], + }), + ); + } + }); + + it('should reject invalid maxConcurrentRequests', () => { + const invalidConfig = { + data: { + tabs: [], + settings: { + maxConcurrentRequests: 0, // less than minimum 1 + }, + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'invalid_union', + path: ['data', 'settings', 'maxConcurrentRequests'], + }), + ); + } + }); + + it('should accept layout item with negative dimensions (schema allows this)', () => { + const validConfig = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [], + layout: [ + { + i: 'item1', + h: -1, + w: 12, + x: 0, + y: 0, + }, + ], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(validConfig); + expect(result.success).toBe(true); + }); + + it('should reject connection with empty from/to fields', () => { + const invalidConfig = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [], + layout: [], + connections: [ + { + from: '', + to: 'widget1', + kind: DashTabConnectionKind.Ignore, + }, + ], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'too_small', + path: ['data', 'tabs', 0, 'connections', 0, 'from'], + }), + ); + } + }); + + it('should reject aliases with invalid structure', () => { + const invalidConfig = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [['single-item']], // should have at least 2 items + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'too_small', + path: ['data', 'tabs', 0, 'aliases', DASH_DEFAULT_NAMESPACE, 0], + }), + ); + } + }); + }); + + describe('edge cases', () => { + it('should handle null values in settings', () => { + const config = { + data: { + tabs: [], + settings: { + autoupdateInterval: null, + maxConcurrentRequests: null, + }, + }, + }; + + const result = dashSchema.safeParse(config); + expect(result.success).toBe(true); + }); + + it('should handle string autoupdate interval', () => { + const config = { + data: { + tabs: [], + settings: { + autoupdateInterval: '60', + }, + }, + }; + + const result = dashSchema.safeParse(config); + expect(result.success).toBe(true); + }); + + it('should handle empty key', () => { + const config = { + key: '', + data: { + tabs: [], + }, + }; + + const result = dashSchema.safeParse(config); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'too_small', + path: ['key'], + }), + ); + } + }); + + it('should handle complex nested structures', () => { + const config: DashSchema = { + data: { + tabs: [ + { + id: 'complex-tab', + title: 'Complex Tab', + items: [ + { + id: 'complex-widget', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Widget, + data: { + hideTitle: false, + tabs: [ + { + id: 'nested-tab-1', + title: 'Nested Tab 1', + description: 'First nested tab', + chartId: 'chart-1', + isDefault: true, + params: { + complexParam: { + nested: { + value: 'deep-value', + array: [1, 2, 3], + }, + }, + }, + autoHeight: false, + }, + { + id: 'nested-tab-2', + title: 'Nested Tab 2', + chartId: 'chart-2', + params: {}, + }, + ], + }, + }, + ], + layout: [ + { + i: 'complex-widget', + h: 10, + w: 24, + x: 6, + y: 5, + }, + ], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [ + ['alias-group-1', 'alias-group-2'], + ['another-alias-1', 'another-alias-2'], + ], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(config); + expect(result.success).toBe(true); + }); + }); +}); diff --git a/src/shared/sdk/zod-shemas/chart-api.schema.ts b/src/shared/sdk/zod-shemas/chart-api.schema.ts new file mode 100644 index 0000000000..f74f8f9c3e --- /dev/null +++ b/src/shared/sdk/zod-shemas/chart-api.schema.ts @@ -0,0 +1,495 @@ +import * as z from 'zod/v4'; + +import { + ColorMode, + GradientNullModes, + IndicatorTitleMode, + LabelsPositions, + MapCenterMode, + ZoomMode, +} from '../../'; +import {WidgetSize} from '../../constants'; +import {MARKUP_TYPE} from '../../types/charts'; +import type {DatasetFieldCalcMode} from '../../types/dataset'; +import { + AxisLabelFormatMode, + AxisMode, + AxisNullsMode, + ChartsConfigVersion, + NumberFormatType, + NumberFormatUnit, +} from '../../types/wizard'; + +// Helper type for enum to literal conversion +type EnumToLiteral = T extends string + ? `${T}` + : `${T}` extends `${infer N extends number}` + ? N + : never; + +type ValueOf = T[keyof T]; + +// Constants for enum arrays - base arrays +const showHideValuesList = ['show', 'hide'] as const; +const onOffValuesList = ['on', 'off'] as const; +const autoManualValuesList = ['auto', 'manual'] as const; +const yesNoValuesList = ['yes', 'no'] as const; + +// Constants for literal values +const SCALE_VALUE_LITERAL = '0-max'; +const LOGARITHMIC_TYPE_LITERAL = 'logarithmic'; +const MANUAL_GRID_STEP_LITERAL = 'manual'; +const DATALENS_TYPE_LITERAL = 'datalens'; + +// Constants for enum arrays - specific arrays +const DatasetFieldCalcModeList: DatasetFieldCalcMode[] = ['formula', 'direct', 'parameter']; +const titleModeList = showHideValuesList; +const legendModeList = showHideValuesList; +const tooltipModeList = showHideValuesList; +const onOffList = onOffValuesList; +const groupingList = ['disabled', 'off'] as const; +const scaleList = autoManualValuesList; +const titleList = ['auto', 'manual', 'off'] as const; +const gridList = onOffValuesList; +const hideLabelsLis = yesNoValuesList; +const labelsViewList = ['horizontal', 'vertical', 'angle'] as const; +const holidaysList = onOffValuesList; +const axisVisibilityList = showHideValuesList; +const colorFieldTitleList = onOffValuesList; +const v12UpdateActionsList = [ + 'add_field', + 'add', + 'update_field', + 'update', + 'delete', + 'delete_field', +] as const; + +// Basic schemas +const parameterDefaultValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +const datasetFieldCalcModeSchema = z.enum(DatasetFieldCalcModeList); + +// Enum schemas using Object.values for regular enums +const colorModeSchema = z.enum(Object.values(ColorMode) as EnumToLiteral[]); +const gradientNullModeSchema = z.enum( + Object.values(GradientNullModes) as ValueOf[], +); +const indicatorTitleModeSchema = z.enum([ + IndicatorTitleMode.ByField, + IndicatorTitleMode.Hide, + IndicatorTitleMode.Manual, +]); +const labelsPositionsSchema = z.enum( + Object.values(LabelsPositions) as EnumToLiteral[], +); +const markupTypeSchema = z.enum( + Object.values(MARKUP_TYPE) as [ + EnumToLiteral<(typeof MARKUP_TYPE)[keyof typeof MARKUP_TYPE]>, + ...EnumToLiteral<(typeof MARKUP_TYPE)[keyof typeof MARKUP_TYPE]>[], + ], +); +const widgetSizeTypeSchema = z.enum( + Object.values(WidgetSize) as [ + EnumToLiteral<(typeof WidgetSize)[keyof typeof WidgetSize]>, + ...EnumToLiteral<(typeof WidgetSize)[keyof typeof WidgetSize]>[], + ], +); + +// Const enum schemas using direct values +const axisLabelFormatModeSchema = z.enum([AxisLabelFormatMode.Auto, AxisLabelFormatMode.ByField]); +const axisModeSchema = z.enum([AxisMode.Discrete, AxisMode.Continuous]); +const axisNullsModeSchema = z.enum([ + AxisNullsMode.Ignore, + AxisNullsMode.Connect, + AxisNullsMode.AsZero, + AxisNullsMode.UsePrevious, +]); +const numberFormatTypeSchema = z.enum([NumberFormatType.Number, NumberFormatType.Percent]); +const numberFormatUnitSchema = z.enum([ + NumberFormatUnit.Auto, + NumberFormatUnit.B, + NumberFormatUnit.K, + NumberFormatUnit.M, + NumberFormatUnit.T, +]); + +// Simple string enums for types that are not available as runtime values +const mapCenterModesSchema = z.enum([MapCenterMode.Auto, MapCenterMode.Manual]); +const zoomModesSchema = z.enum([ZoomMode.Auto, ZoomMode.Manual]); + +// V12ClientOnlyFields schema +const v12ClientOnlyFieldsSchema = z.object({ + fakeTitle: z.string().optional(), + originalTitle: z.string().optional(), + markupType: markupTypeSchema.optional(), +}); + +// V12Formatting schema +const v12FormattingSchema = z.object({ + format: numberFormatTypeSchema.optional(), + showRankDelimiter: z.boolean().optional(), + prefix: z.string().optional(), + postfix: z.string().optional(), + unit: numberFormatUnitSchema.optional(), + precision: z.number().optional(), + labelMode: z.string().optional(), +}); + +// V12NavigatorSettings schema +const v12NavigatorSettingsSchema = z.object({ + navigatorMode: z.string(), + isNavigatorAvailable: z.boolean(), + selectedLines: z.array(z.string()), + linesMode: z.string(), + periodSettings: z.object({ + type: z.string(), + value: z.string(), + period: z.string(), + }), +}); + +// V12CommonSharedExtraSettings schema +const v12CommonSharedExtraSettingsSchema = z.object({ + title: z.string().optional(), + titleMode: z.enum(titleModeList).optional(), + indicatorTitleMode: indicatorTitleModeSchema.optional(), + legendMode: z.enum(legendModeList).optional(), + metricFontSize: z.string().optional(), + metricFontColor: z.string().optional(), + tooltip: z.enum(tooltipModeList).optional(), + tooltipSum: z.enum(onOffList).optional(), + limit: z.number().optional(), + pagination: z.enum(onOffList).optional(), + navigatorMode: z.string().optional(), + navigatorSeriesName: z.string().optional(), + totals: z.enum(onOffList).optional(), + pivotFallback: z.enum(onOffList).optional(), + pivotInlineSort: z.enum(onOffList).optional(), + stacking: z.enum(onOffList).optional(), + overlap: z.enum(onOffList).optional(), + feed: z.string().optional(), + navigatorSettings: v12NavigatorSettingsSchema.optional(), + enableGPTInsights: z.boolean().optional(), + labelsPosition: labelsPositionsSchema.optional(), + pinnedColumns: z.number().optional(), + size: widgetSizeTypeSchema.optional(), + zoomMode: zoomModesSchema.optional(), + zoomValue: z.number().nullable().optional(), + mapCenterMode: mapCenterModesSchema.optional(), + mapCenterValue: z.string().nullable().optional(), + preserveWhiteSpace: z.boolean().optional(), +}); + +// V12Filter schema +const v12FilterSchema = z + .object({ + guid: z.string(), + datasetId: z.string(), + disabled: z.string().optional(), + filter: z.object({ + operation: z.object({ + code: z.string(), + }), + value: z.union([z.string(), z.array(z.string())]).optional(), + }), + type: z.string(), + title: z.string(), + calc_mode: datasetFieldCalcModeSchema, + }) + .merge(v12ClientOnlyFieldsSchema); + +// V12Sort schema +const v12SortSchema = z + .object({ + guid: z.string(), + title: z.string(), + source: z.string().optional(), + datasetId: z.string(), + direction: z.string(), + data_type: z.string(), + format: z.string().optional(), + type: z.string(), + default_value: parameterDefaultValueSchema.optional(), + }) + .merge(v12ClientOnlyFieldsSchema); + +// V12LinkField schema +const v12LinkFieldSchema = z.object({ + field: z.object({ + title: z.string(), + guid: z.string(), + }), + dataset: z.object({ + id: z.string(), + realName: z.string(), + }), +}); + +// V12Link schema +const v12LinkSchema = z.object({ + id: z.string(), + fields: z.record(z.string(), v12LinkFieldSchema), +}); + +// V12PlaceholderSettings schema +const v12PlaceholderSettingsSchema = z.object({ + groupping: z.enum(groupingList).optional(), + autoscale: z.boolean().optional(), + scale: z.enum(scaleList).optional(), + scaleValue: z + .union([z.literal(SCALE_VALUE_LITERAL), z.tuple([z.string(), z.string()])]) + .optional(), + title: z.enum(titleList).optional(), + titleValue: z.string().optional(), + type: z.literal(LOGARITHMIC_TYPE_LITERAL).optional(), + grid: z.enum(gridList).optional(), + gridStep: z.literal(MANUAL_GRID_STEP_LITERAL).optional(), + gridStepValue: z.number().optional(), + hideLabels: z.enum(hideLabelsLis).optional(), + labelsView: z.enum(labelsViewList).optional(), + nulls: axisNullsModeSchema.optional(), + holidays: z.enum(holidaysList).optional(), + axisFormatMode: axisLabelFormatModeSchema.optional(), + axisModeMap: z.record(z.string(), axisModeSchema).optional(), + disableAxisMode: z.boolean().optional(), + axisVisibility: z.enum(axisVisibilityList).optional(), +}); + +// Forward declaration for recursive types +const v12FieldSchema: z.ZodType = z.lazy(() => + z + .object({ + data_type: z.string(), + fields: z.array(v12FieldSchema).optional(), + type: z.string(), + title: z.string(), + guid: z.string(), + formatting: v12FormattingSchema.optional(), + format: z.string().optional(), + datasetId: z.string(), + source: z.string().optional(), + datasetName: z.string().optional(), + hideLabelMode: z.string().optional(), + calc_mode: datasetFieldCalcModeSchema, + default_value: parameterDefaultValueSchema.optional(), + barsSettings: z.any().optional(), // TableBarsSettings + subTotalsSettings: z.any().optional(), // TableSubTotalsSettings + backgroundSettings: z.any().optional(), // TableFieldBackgroundSettings + columnSettings: z.any().optional(), // ColumnSettings + hintSettings: z.any().optional(), // HintSettings + }) + .merge(v12ClientOnlyFieldsSchema), +); + +// V12Placeholder schema +const v12PlaceholderSchema = z.object({ + id: z.string(), + settings: v12PlaceholderSettingsSchema.optional(), + required: z.boolean().optional(), + capacity: z.number().optional(), + items: z.array(v12FieldSchema), +}); + +// V12Color schema +const v12ColorSchema = z + .object({ + datasetId: z.string(), + guid: z.string(), + title: z.string(), + type: z.string(), + data_type: z.string(), + formatting: v12FormattingSchema.optional(), + calc_mode: datasetFieldCalcModeSchema, + }) + .merge(v12ClientOnlyFieldsSchema); + +// V12Shape schema +const v12ShapeSchema = z + .object({ + datasetId: z.string(), + guid: z.string(), + title: z.string(), + originalTitle: z.string().optional(), + type: z.string(), + data_type: z.string(), + calc_mode: datasetFieldCalcModeSchema, + }) + .merge(v12ClientOnlyFieldsSchema); + +// V12Tooltip schema +const v12TooltipSchema = z + .object({ + datasetId: z.string(), + guid: z.string(), + title: z.string(), + formatting: v12FormattingSchema.optional(), + data_type: z.string(), + calc_mode: datasetFieldCalcModeSchema, + }) + .merge(v12ClientOnlyFieldsSchema); + +// V12Label schema +const v12LabelSchema = z.object({ + datasetId: z.string(), + type: z.string(), + title: z.string(), + guid: z.string(), + formatting: v12FormattingSchema.optional(), + format: z.string().optional(), + data_type: z.string(), + calc_mode: datasetFieldCalcModeSchema, +}); + +// V12HierarchyField schema +const v12HierarchyFieldSchema = z.object({ + data_type: z.string(), + fields: z.array(v12FieldSchema), + type: z.string(), +}); + +// V12PointSizeConfig schema +const v12PointSizeConfigSchema = z.object({ + radius: z.number(), + minRadius: z.number(), + maxRadius: z.number(), +}); + +// V12ColorsConfig schema +const v12ColorsConfigSchema = z.object({ + thresholdsMode: z.string().optional(), + leftThreshold: z.string().optional(), + middleThreshold: z.string().optional(), + rightThreshold: z.string().optional(), + gradientPalette: z.string().optional(), + gradientMode: z.string().optional(), + polygonBorders: z.string().optional(), + reversed: z.boolean().optional(), + fieldGuid: z.string().optional(), + mountedColors: z.record(z.string(), z.string()).optional(), + coloredByMeasure: z.boolean().optional(), + palette: z.string().optional(), + colorMode: colorModeSchema.optional(), + nullMode: gradientNullModeSchema.optional(), +}); + +// V12ShapesConfig schema +const v12ShapesConfigSchema = z.object({ + mountedShapes: z.record(z.string(), z.string()).optional(), + fieldGuid: z.string().optional(), +}); + +// V12TooltipConfig schema +const v12TooltipConfigSchema = z.object({ + color: z.enum(colorFieldTitleList).optional(), + fieldTitle: z.enum(colorFieldTitleList).optional(), +}); + +// V12LayerSettings schema +const v12LayerSettingsSchema = z.object({ + id: z.string(), + name: z.string(), + type: z.string(), + alpha: z.number(), + valid: z.boolean(), +}); + +// V12CommonPlaceholders schema +const v12CommonPlaceholdersSchema = z.object({ + colors: z.array(v12ColorSchema), + labels: z.array(v12LabelSchema), + tooltips: z.array(v12TooltipSchema), + filters: z.array(v12FilterSchema), + sort: z.array(v12SortSchema), + shapes: z.array(v12ShapeSchema).optional(), + colorsConfig: v12ColorsConfigSchema.optional(), + geopointsConfig: v12PointSizeConfigSchema.optional(), + shapesConfig: v12ShapesConfigSchema.optional(), + tooltipConfig: v12TooltipConfigSchema.optional(), +}); + +// V12Layer schema +const v12LayerSchema = z.object({ + id: z.string(), + commonPlaceholders: v12CommonPlaceholdersSchema, + layerSettings: v12LayerSettingsSchema, + placeholders: z.array(v12PlaceholderSchema), +}); + +// V12Visualization schema +const v12VisualizationSchema = z.object({ + id: z.string(), + highchartsId: z.string().optional(), + selectedLayerId: z.string().optional(), + layers: z.array(v12LayerSchema).optional(), + placeholders: z.array(v12PlaceholderSchema), +}); + +// V12Update schema +const v12UpdateSchema = z.object({ + action: z.enum(v12UpdateActionsList), + field: z.any(), + debug_info: z.string().optional(), +}); + +// V12ChartsConfigDatasetField schema +const v12ChartsConfigDatasetFieldSchema = z.object({ + guid: z.string(), + title: z.string(), + calc_mode: datasetFieldCalcModeSchema.optional(), +}); + +// Main V12ChartsConfig schema +export const v12ChartsConfigSchema = z.object({ + title: z.string().optional(), + colors: z.array(v12ColorSchema), + colorsConfig: v12ColorsConfigSchema.optional(), + extraSettings: v12CommonSharedExtraSettingsSchema.optional(), + filters: z.array(v12FilterSchema), + geopointsConfig: v12PointSizeConfigSchema.optional(), + hierarchies: z.array(v12HierarchyFieldSchema), + labels: z.array(v12LabelSchema), + links: z.array(v12LinkSchema), + sort: z.array(v12SortSchema), + tooltips: z.array(v12TooltipSchema), + tooltipConfig: v12TooltipConfigSchema.optional(), + type: z.literal(DATALENS_TYPE_LITERAL), + updates: z.array(v12UpdateSchema), + visualization: v12VisualizationSchema, + shapes: z.array(v12ShapeSchema), + shapesConfig: v12ShapesConfigSchema.optional(), + version: z.literal(ChartsConfigVersion.V12), + datasetsIds: z.array(z.string()), + datasetsPartialFields: z.array(z.array(v12ChartsConfigDatasetFieldSchema)), + segments: z.array(v12FieldSchema), + chartType: z.string().optional(), +}); + +// Export JSON schema generation +export const chartApiValidationJsonSchema = z.toJSONSchema(v12ChartsConfigSchema); + +// Export the TypeScript type +export type V12ChartsConfigSchema = z.infer; + +// Export individual schemas for potential reuse +export { + v12ColorSchema, + v12ShapeSchema, + v12TooltipSchema, + v12LabelSchema, + v12FilterSchema, + v12SortSchema, + v12LinkSchema, + v12VisualizationSchema, + v12UpdateSchema, + v12FieldSchema, + v12HierarchyFieldSchema, + v12PointSizeConfigSchema, + v12ColorsConfigSchema, + v12ShapesConfigSchema, + v12TooltipConfigSchema, + v12CommonSharedExtraSettingsSchema, + v12NavigatorSettingsSchema, + v12FormattingSchema, + v12ClientOnlyFieldsSchema, + v12ChartsConfigDatasetFieldSchema, +}; diff --git a/src/shared/sdk/zod-shemas/dash-api.schema.ts b/src/shared/sdk/zod-shemas/dash-api.schema.ts new file mode 100644 index 0000000000..0767628a86 --- /dev/null +++ b/src/shared/sdk/zod-shemas/dash-api.schema.ts @@ -0,0 +1,275 @@ +import * as z from 'zod/v4'; + +import { + CONTROLS_PLACEMENT_MODE, + DASH_CURRENT_SCHEME_VERSION, + DashLoadPriority, + DashTabConnectionKind, + DashTabItemControlElementType, + DashTabItemControlSourceType, + DashTabItemTitleSizes, + DashTabItemType, +} from '../../'; +const DASH_DEFAULT_NAMESPACE = 'default'; + +// Text definition +const textSchema = z.object({ + text: z.string(), +}); + +// Title definition +const titleSchema = z.object({ + text: z.string(), + size: z.enum(DashTabItemTitleSizes), + showInTOC: z.boolean().optional(), +}); + +// Widget definition +const widgetSchema = z.object({ + hideTitle: z.boolean().optional(), + tabs: z.array( + z.object({ + id: z.string().min(1), + title: z.string().min(1), + description: z.string().optional(), + chartId: z.string().min(1).optional(), + isDefault: z.boolean().optional(), + params: z.record(z.any(), z.any()).optional(), + autoHeight: z.boolean().optional(), + }), + ), +}); + +// Control element type definition +const controlElementTypeSchema = z + .object({ + required: z.boolean().optional(), + showHint: z.boolean().optional(), + showTitle: z.boolean().optional(), + elementType: z.enum(DashTabItemControlElementType), + }) + .and( + z.discriminatedUnion('elementType', [ + z.object({ + elementType: z.literal(DashTabItemControlElementType.Select), + defaultValue: z.union([z.string(), z.array(z.string()), z.null()]).optional(), + multiselectable: z.boolean().optional(), + }), + z.object({ + elementType: z.literal(DashTabItemControlElementType.Date), + defaultValue: z.string().optional(), + isRange: z.boolean().optional(), + }), + z.object({ + elementType: z.literal(DashTabItemControlElementType.Input), + defaultValue: z.string().optional(), + }), + z.object({ + elementType: z.literal(DashTabItemControlElementType.Checkbox), + defaultValue: z.string().optional(), + }), + ]), + ); + +// Control source dataset definition +const controlSourceDatasetSchema = controlElementTypeSchema.and( + z.object({ + datasetId: z.string().min(1), + datasetFieldId: z.string().min(1), + }), +); + +// Control source manual definition +const controlSourceManualSchema = controlElementTypeSchema.and( + z.object({ + fieldName: z.string().min(1), + acceptableValues: z + .union([ + // elementType: select + z.array( + z.object({ + value: z.string(), + title: z.string(), + }), + ), + // elementType: date + z.object({ + from: z.string().optional(), + to: z.string().optional(), + }), + ]) + .optional(), + }), +); + +// Control source external definition +const controlSourceExternalSchema = z.object({ + chartId: z.string().min(1), + text: z.string().optional(), + autoHeight: z.boolean().optional(), +}); + +// Control definition +const controlSchema = z + .object({ + title: z.string().min(1), + sourceType: z.enum(DashTabItemControlSourceType), + }) + .and( + z.discriminatedUnion('sourceType', [ + z.object({ + sourceType: z.literal(DashTabItemControlSourceType.Dataset), + source: controlSourceDatasetSchema, + }), + z.object({ + sourceType: z.literal(DashTabItemControlSourceType.Manual), + source: controlSourceManualSchema, + }), + z.object({ + sourceType: z.literal(DashTabItemControlSourceType.External), + source: controlSourceExternalSchema, + }), + ]), + ); + +// Group control items definition +const groupControlItemsSchema = z + .object({ + id: z.string().min(1), + title: z.string().min(1), + namespace: z.literal(DASH_DEFAULT_NAMESPACE), + sourceType: z.union([ + z.literal(DashTabItemControlSourceType.Dataset), + z.literal(DashTabItemControlSourceType.Manual), + ]), + defaults: z.record(z.any(), z.any()), + placementMode: z.enum(CONTROLS_PLACEMENT_MODE).optional(), + width: z.string().optional(), + }) + .and( + z.discriminatedUnion('sourceType', [ + z.object({ + sourceType: z.literal(DashTabItemControlSourceType.Dataset), + source: controlSourceDatasetSchema, + }), + z.object({ + sourceType: z.literal(DashTabItemControlSourceType.Manual), + source: controlSourceManualSchema, + }), + ]), + ); + +// Group control definition +const groupControlSchema = z.object({ + group: z.array(groupControlItemsSchema), + autoHeight: z.boolean().optional(), + buttonApply: z.boolean().optional(), + buttonReset: z.boolean().optional(), + showGroupName: z.boolean().optional(), + updateControlsOnChange: z.boolean().optional(), +}); + +// Layout item definition +const layoutItemSchema = z.object({ + i: z.string().min(1), + h: z.number(), + w: z.number(), + x: z.number(), + y: z.number(), + parent: z.string().optional(), +}); + +// Connection definition +const connectionSchema = z.object({ + from: z.string().min(1), + to: z.string().min(1), + kind: z.enum(DashTabConnectionKind), +}); + +// Tab item definition +const tabItemSchema = z + .object({ + id: z.string().min(1), + namespace: z.literal(DASH_DEFAULT_NAMESPACE), + type: z.enum(DashTabItemType), + }) + .and( + z.discriminatedUnion('type', [ + z.object({ + type: z.literal(DashTabItemType.Text), + data: textSchema, + }), + z.object({ + type: z.literal(DashTabItemType.Title), + data: titleSchema, + }), + z.object({ + type: z.literal(DashTabItemType.Widget), + data: widgetSchema, + }), + z.object({ + type: z.literal(DashTabItemType.Control), + data: controlSchema, + defaults: z.record(z.any(), z.any()), + }), + z.object({ + type: z.literal(DashTabItemType.GroupControl), + data: groupControlSchema, + }), + ]), + ); + +// Alias definition +const aliasRecordSchema = z.array(z.string().min(1)).max(2).min(2); + +// Tab definition +const tabSchema = z + .object({ + id: z.string().min(1), + title: z.string().min(1), + items: z.array(tabItemSchema), + layout: z.array(layoutItemSchema), + connections: z.array(connectionSchema), + aliases: z + .object({ + [DASH_DEFAULT_NAMESPACE]: z.array(aliasRecordSchema).optional(), + }) + .strict(), + }) + .strict(); + +// Settings definition +const settingsSchema = z.object({ + autoupdateInterval: z.union([z.number().min(30), z.string(), z.null()]).optional(), + maxConcurrentRequests: z.union([z.number().min(1), z.null()]).optional(), + loadPriority: z.enum(DashLoadPriority).optional(), + silentLoading: z.boolean().optional(), + dependentSelectors: z.boolean().optional(), + globalParams: z.record(z.any(), z.any()).optional(), + hideTabs: z.boolean().optional(), + hideDashTitle: z.boolean().optional(), + expandTOC: z.boolean().optional(), + assistantEnabled: z.boolean().optional(), +}); + +// Data definition +const dataSchema = z.object({ + counter: z.number().int().min(1).optional(), + salt: z.string().min(1).optional(), + schemeVersion: z.number().int().min(1).max(DASH_CURRENT_SCHEME_VERSION).optional(), + tabs: z.array(tabSchema), + settings: settingsSchema.optional(), +}); + +// Main dashboard API validation schema +export const dashSchema = z.object({ + key: z.string().min(1).optional(), + data: dataSchema, + meta: z.record(z.any(), z.any()).optional(), + links: z.record(z.string(), z.string()).optional(), +}); + +export const dashApiValidationJsonSchema = z.toJSONSchema(dashSchema); + +// Export the type for TypeScript usage +export type DashSchema = z.infer; diff --git a/src/shared/sdk/zod-shemas/dataset-api.schema.ts b/src/shared/sdk/zod-shemas/dataset-api.schema.ts new file mode 100644 index 0000000000..bd38e32160 --- /dev/null +++ b/src/shared/sdk/zod-shemas/dataset-api.schema.ts @@ -0,0 +1,339 @@ +import * as z from 'zod/v4'; + +import { + DATASET_FIELD_TYPES, + DATASET_VALUE_CONSTRAINT_TYPE, + DatasetFieldAggregation, + DatasetFieldType, +} from '../../types/dataset'; + +// Basic type schemas +const parameterDefaultValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); + +const datasetRlsSchema = z.record(z.string(), z.string()); + +// Dataset field aggregation schema +const datasetFieldAggregationSchema = z.enum(DatasetFieldAggregation); + +// Dataset field type schema +const datasetFieldTypeSchema = z.enum(DatasetFieldType); + +// Dataset field types schema +const datasetFieldTypesSchema = z.enum(DATASET_FIELD_TYPES); + +// Dataset field calc mode schema +const datasetFieldCalcModeSchema = z.union([ + z.literal('formula'), + z.literal('direct'), + z.literal('parameter'), +]); + +// Dataset value constraint schema +const datasetValueConstraintSchema = z.discriminatedUnion('type', [ + z.object({ + type: z.literal(DATASET_VALUE_CONSTRAINT_TYPE.DEFAULT), + }), + z.object({ + type: z.literal(DATASET_VALUE_CONSTRAINT_TYPE.NULL), + }), + z.object({ + type: z.literal(DATASET_VALUE_CONSTRAINT_TYPE.REGEX), + pattern: z.string(), + }), +]); + +// Dataset field schema +const datasetFieldSchema = z.object({ + aggregation: datasetFieldAggregationSchema, + type: datasetFieldTypeSchema, + calc_mode: datasetFieldCalcModeSchema, + default_value: parameterDefaultValueSchema, + initial_data_type: datasetFieldTypesSchema, + cast: datasetFieldTypesSchema, + data_type: datasetFieldTypesSchema, + description: z.string(), + guid: z.string(), + title: z.string(), + managed_by: z.string(), + source: z.string(), + avatar_id: z.string(), + formula: z.string().optional(), + guid_formula: z.string().optional(), + has_auto_aggregation: z.boolean(), + aggregation_locked: z.boolean(), + lock_aggregation: z.boolean(), + virtual: z.boolean(), + valid: z.boolean(), + hidden: z.boolean(), + autoaggregated: z.boolean(), + template_enabled: z.boolean().optional(), + value_constraint: datasetValueConstraintSchema.optional(), +}); + +// Dataset field error schema +const datasetFieldErrorSchema = z.object({ + guid: z.string(), + title: z.string(), + errors: z.array( + z.object({ + column: z.union([z.number(), z.null()]), + row: z.union([z.number(), z.null()]), + message: z.string(), + }), + ), +}); + +// Dataset component error item schema +const datasetComponentErrorItemSchema = z.object({ + code: z.string(), + level: z.string(), + message: z.string(), + details: z.object({ + db_message: z.string().optional(), + query: z.string().optional(), + }), +}); + +// Dataset component error schema +const datasetComponentErrorSchema = z.object({ + id: z.string(), + type: z.union([z.literal('data_source'), z.literal('field')]), + errors: z.array(datasetComponentErrorItemSchema), +}); + +// Obligatory default filter schema +const obligatoryDefaultFilterSchema = z.object({ + column: z.string(), + operation: z.string(), + values: z.array(z.string()), +}); + +// Obligatory filter schema +const obligatoryFilterSchema = z.object({ + id: z.string(), + field_guid: z.string(), + managed_by: z.string(), + valid: z.boolean(), + default_filters: z.array(obligatoryDefaultFilterSchema), +}); + +// Dataset raw schema +const datasetRawSchemaSchema = z.object({ + user_type: z.string(), + name: z.string(), + title: z.string(), + description: z.string(), + nullable: z.boolean(), + lock_aggregation: z.boolean(), + has_auto_aggregation: z.boolean(), + native_type: z.object({ + name: z.string(), + conn_type: z.string(), + }), +}); + +// Dataset source schema +const datasetSourceSchema = z.object({ + id: z.string(), + connection_id: z.string(), + ref_source_id: z.union([z.string(), z.null()]), + name: z.string(), + title: z.string(), + source_type: z.string(), + managed_by: z.string(), + parameter_hash: z.string(), + valid: z.boolean(), + is_ref: z.boolean(), + virtual: z.boolean(), + raw_schema: z.array(datasetRawSchemaSchema), + group: z.array(z.string()), + parameters: z.object({ + table_name: z.string(), + db_version: z.string(), + db_name: z.union([z.string(), z.null()]), + }), +}); + +// Dataset source avatar schema +const datasetSourceAvatarSchema = z.object({ + id: z.string(), + title: z.string(), + source_id: z.string(), + managed_by: z.string(), + valid: z.boolean(), + is_root: z.boolean(), + virtual: z.boolean(), +}); + +// Dataset avatar relation condition schema +const datasetAvatarRelationConditionSchema = z.object({ + operator: z.string(), + type: z.string(), + left: z.object({ + calc_mode: z.string(), + source: z.union([z.string(), z.null()]), + }), + right: z.object({ + calc_mode: z.string(), + source: z.union([z.string(), z.null()]), + }), +}); + +// Dataset avatar relation schema +const datasetAvatarRelationSchema = z.object({ + id: z.string(), + join_type: z.string(), + left_avatar_id: z.string(), + right_avatar_id: z.string(), + managed_by: z.string(), + virtual: z.boolean(), + conditions: z.array(datasetAvatarRelationConditionSchema), + required: z.boolean(), +}); + +// Dataset option data type item schema +const datasetOptionDataTypeItemSchema = z.object({ + aggregations: z.array(datasetFieldAggregationSchema), + casts: z.array(datasetFieldTypesSchema), + type: z.string(), + filter_operations: z.array(z.string()), +}); + +// Dataset option field item schema +const datasetOptionFieldItemSchema = z.object({ + aggregations: z.array(datasetFieldAggregationSchema), + casts: z.array(datasetFieldTypesSchema), + guid: z.string(), +}); + +// Dataset options schema +const datasetOptionsSchema = z.object({ + connections: z.object({ + compatible_types: z.array(z.string()), + items: z.array( + z.object({ + id: z.string(), + replacement_types: z.array( + z.object({ + conn_type: z.string(), // ConnectorType but using string for flexibility + }), + ), + }), + ), + max: z.number(), + }), + syntax_highlighting_url: z.string(), + sources: z.object({ + compatible_types: z.array(z.string()), + items: z.array( + z.object({ + schema_update_enabled: z.boolean(), + id: z.string(), + }), + ), + }), + preview: z.object({ + enabled: z.boolean(), + }), + source_avatars: z.object({ + items: z.array( + z.object({ + schema_update_enabled: z.boolean(), + id: z.string(), + }), + ), + max: z.number(), + }), + schema_update_enabled: z.boolean(), + supports_offset: z.boolean(), + supported_functions: z.array(z.string()), + data_types: z.object({ + items: z.array(datasetOptionDataTypeItemSchema), + }), + fields: z.object({ + items: z.array(datasetOptionFieldItemSchema), + }), + join: z.object({ + types: z.array(z.string()), + operators: z.array(z.string()), + }), +}); + +// Dataset API error schema +const datasetApiErrorSchema = z.object({ + datasetId: z.string(), + error: z.object({ + code: z.string().optional(), + message: z.string().optional(), + }), +}); + +// Dataset selection map schema +const datasetSelectionMapSchema = z.record(z.string(), z.literal(true)); + +// Main Dataset schema +const datasetSchema = z.object({ + id: z.string(), + realName: z.string(), + is_favorite: z.boolean(), + key: z.string(), + options: datasetOptionsSchema, + dataset: z.object({ + avatar_relations: z.array(datasetAvatarRelationSchema), + component_errors: z.object({ + items: z.array(datasetComponentErrorSchema), + }), + obligatory_filters: z.array(obligatoryFilterSchema), + preview_enabled: z.boolean(), + result_schema: z.array(datasetFieldSchema), + result_schema_aux: z.object({ + inter_dependencies: z.object({ + deps: z.array(z.string()), + }), + }), + rls: datasetRlsSchema, + rls2: z.array(z.unknown()), + source_avatars: z.array(datasetSourceAvatarSchema), + source_features: z.record(z.string(), z.any()), + sources: z.array(datasetSourceSchema), + revisionId: z.string(), + load_preview_by_default: z.boolean(), + template_enabled: z.boolean(), + data_export_forbidden: z.boolean().optional(), + }), + workbook_id: z.string().optional(), + permissions: z.any().optional(), // Using z.any() for Permissions type as it's complex + + // Backward compatibility fields + avatar_relations: z.array(datasetAvatarRelationSchema), + component_errors: z.object({ + items: z.array(datasetComponentErrorSchema), + }), + preview_enabled: z.boolean(), + raw_schema: z.array(datasetRawSchemaSchema).optional(), + result_schema: z.array(datasetFieldSchema).optional(), + rls: datasetRlsSchema, + source_avatars: z.array(datasetSourceAvatarSchema), + source_features: z.record(z.string(), z.any()), + sources: z.array(datasetSourceSchema), +}); + +// Export JSON schema generation +export const datasetApiValidationJsonSchema = z.toJSONSchema(datasetSchema); + +// Export the TypeScript type +export type DatasetApiValidationType = z.infer; + +// Export individual schemas for potential reuse +export { + datasetFieldSchema, + datasetSourceSchema, + datasetOptionsSchema, + datasetAvatarRelationSchema, + datasetSourceAvatarSchema, + obligatoryFilterSchema, + datasetComponentErrorSchema, + datasetFieldErrorSchema, + datasetApiErrorSchema, + datasetSelectionMapSchema, +}; From f6b1eb11da29062a31ebad1f7078d0f7066ceea9 Mon Sep 17 00:00:00 2001 From: Serge Pavlyuk Date: Wed, 23 Jul 2025 16:08:19 +0300 Subject: [PATCH 02/40] Added public-api export --- src/server/constants/public-api.ts | 1 + src/server/controllers/index.ts | 9 +- src/server/controllers/public-api.ts | 114 +++++++++++++++++ src/server/registry/index.ts | 49 ++++++- src/server/types/controllers.ts | 3 +- src/server/types/public-api.ts | 18 +++ src/server/utils/index.ts | 7 + src/server/utils/routes.ts | 7 + src/shared/components/api-docs/index.ts | 0 src/shared/schema/bi/actions/datasets.ts | 22 +++- src/shared/schema/gateway-utils.ts | 121 +++++++++++++++++- .../sdk/zod-shemas/dataset-api.schema.ts | 2 +- 12 files changed, 347 insertions(+), 6 deletions(-) create mode 100644 src/server/constants/public-api.ts create mode 100644 src/server/controllers/public-api.ts create mode 100644 src/server/types/public-api.ts create mode 100644 src/shared/components/api-docs/index.ts diff --git a/src/server/constants/public-api.ts b/src/server/constants/public-api.ts new file mode 100644 index 0000000000..e38f35de71 --- /dev/null +++ b/src/server/constants/public-api.ts @@ -0,0 +1 @@ +export const PUBLIC_API_RPC_ERROR_CODE = 'ERR.UI_API.PUBLIC-API.FAILED_RPC_PROXY'; diff --git a/src/server/controllers/index.ts b/src/server/controllers/index.ts index a572cd10ab..24cfbbc4cd 100644 --- a/src/server/controllers/index.ts +++ b/src/server/controllers/index.ts @@ -2,5 +2,12 @@ import {apiControllers} from './api'; import {dlMainController} from './dl-main'; import {navigateController} from './navigate'; import {navigationController} from './navigation'; +import {publicApiControllerGetter} from './public-api'; -export {apiControllers, dlMainController, navigateController, navigationController}; +export { + apiControllers, + dlMainController, + navigateController, + navigationController, + publicApiControllerGetter, +}; diff --git a/src/server/controllers/public-api.ts b/src/server/controllers/public-api.ts new file mode 100644 index 0000000000..32dab15247 --- /dev/null +++ b/src/server/controllers/public-api.ts @@ -0,0 +1,114 @@ +import type {Request, Response} from '@gravity-ui/expresskit'; +import {REQUEST_ID_PARAM_NAME} from '@gravity-ui/nodekit'; +import _ from 'lodash'; + +import {getValidationSchema, hasValidationSchema} from '../../shared/schema/gateway-utils'; +import {PUBLIC_API_RPC_ERROR_CODE} from '../constants/public-api'; +import {registry} from '../registry'; +import type {DatalensGatewaySchemas} from '../types/gateway'; +import type {PublicApiRpcMap} from '../types/public-api'; +import Utils from '../utils'; + +const proxyMap: PublicApiRpcMap = { + v0: { + getDataset: { + resolve: (api) => api.bi.getDataset, + }, + updateDataset: { + resolve: (api) => api.bi.updateDataset, + }, + createDataset: { + resolve: (api) => api.bi.createDataset, + }, + deleteDataset: { + resolve: (api) => api.bi.deleteDataset, + }, + }, +}; + +const handleError = (req: Request, res: Response, status: number, message: string) => { + res.status(status).send({ + status, + code: PUBLIC_API_RPC_ERROR_CODE, + message, + requestId: req.ctx.get(REQUEST_ID_PARAM_NAME) || '', + }); +}; + +const parseRoute = (route: string) => { + const spacerIndex = route.indexOf(' '); + const method = route.slice(0, spacerIndex).trim(); + const url = route.slice(spacerIndex).trim(); + + return { + method, + url, + reverse: (props: {version: string; action: string}) => { + return url.replace(':version', props.version).replace(':action', props.action); + }, + }; +}; + +export function publicApiControllerGetter( + gatewayProxyMap: PublicApiRpcMap = proxyMap, + params: any, +) { + const parsedRoute = parseRoute(params.route); + const {gatewayApi} = registry.getGatewayApi(); + + Object.entries(gatewayProxyMap).forEach(([version, actions]) => { + Object.entries(actions).forEach(([action, {resolve}]) => { + if (hasValidationSchema(resolve(gatewayApi))) { + console.log( + parsedRoute.reverse({version, action}), + getValidationSchema(resolve(gatewayApi)).getOpenApichema(), + ); + } + }); + }); + + return async function publicApiController(req: Request, res: Response) { + const boundeHandler = handleError.bind(null, req, res); + + if (!req.params.version || !req.params.action) { + return boundeHandler(400, 'Invalid params, version or action are empty'); + } + + const version = req.params.version as keyof PublicApiRpcMap; + if (!_.has(gatewayProxyMap, version)) { + return boundeHandler(404, 'Version not found'); + } + + const versionMap = gatewayProxyMap[version]; + const actionName = req.params.action as keyof typeof versionMap; + if (!_.has(gatewayProxyMap[version], req.params.action)) { + return boundeHandler(404, 'Action not found'); + } + + try { + const action = versionMap[actionName]; + const {ctx} = req; + + const initialHeaders = Utils.pickRpcHeaders(req); + const headers = action.headers ? action.headers(req, initialHeaders) : initialHeaders; + const args = action.args ? await action.args(req) : req.body; + const requestId = ctx.get(REQUEST_ID_PARAM_NAME) || ''; + + const result = await action.resolve(gatewayApi)({ + headers, + args, + ctx, + requestId, + }); + + res.status(200).send(result); + } catch (err) { + const {error} = err as any; + if (error) { + res.status(typeof error.status === 'number' ? error.status : 500).send(error); + } else { + return boundeHandler(500, 'Unknown error'); + } + } + }; +} diff --git a/src/server/registry/index.ts b/src/server/registry/index.ts index 2276561fd9..b9aace5aae 100644 --- a/src/server/registry/index.ts +++ b/src/server/registry/index.ts @@ -3,7 +3,9 @@ import type {ExpressKit, Request, Response} from '@gravity-ui/expresskit'; import type {ApiWithRoot, GatewayConfig, SchemasByScope} from '@gravity-ui/gateway'; import {getGatewayControllers} from '@gravity-ui/gateway'; import type {AppContext} from '@gravity-ui/nodekit'; +import _ from 'lodash'; +import {getValidationSchema, registerValidationSchema} from '../../shared/schema/gateway-utils'; import type {ChartsEngine} from '../components/charts-engine'; import type {QLConnectionTypeMap} from '../modes/charts/plugins/ql/utils/connection'; import {getConnectorToQlConnectionTypeMap} from '../modes/charts/plugins/ql/utils/connection'; @@ -18,9 +20,45 @@ let chartsEngine: ChartsEngine; export const wrapperGetGatewayControllers = ( schemasByScope: SchemasByScope, config: GatewayConfig, -) => getGatewayControllers(schemasByScope, config); +) => { + const typedSchemasMap = Object.keys(schemasByScope).reduce>( + (memo, scope) => { + const services = schemasByScope[scope]; + Object.keys(services).forEach((service) => { + const actions = services[service].actions; + + Object.entries(actions).forEach(([action, actionConfig]) => { + const validationSchema = getValidationSchema(actionConfig); + + if (validationSchema) { + memo[`${scope}.${service}.${action}`] = validationSchema; + } + }); + }); + + return memo; + }, + {}, + ); + + const controllers = getGatewayControllers( + schemasByScope, + config, + ); + + Object.entries(typedSchemasMap).forEach(([actionPath, schema]) => { + const actionCallback = _.get(controllers.api, actionPath, null); + + if (actionCallback) { + registerValidationSchema(actionCallback, schema); + } + }); + + return controllers; +}; let gateway: ReturnType; +let publicSchema: any; let getLayoutConfig: GetLayoutConfig | undefined; let yfmPlugins: MarkdownItPluginCb[]; let getXlsxConverter: XlsxConverterFn | undefined; @@ -55,11 +93,13 @@ export const registry = { setupGateway( config: GatewayConfig, schemasByScope: SchemasByScope, + publicSchemaArg?: any, // TODO @flops ) { if (gateway) { throw new Error('The method must not be called more than once'); } gateway = wrapperGetGatewayControllers(schemasByScope, config); + publicSchema = publicSchemaArg; }, getGatewayController() { if (!gateway) { @@ -77,6 +117,13 @@ export const registry = { gatewayApi: ApiWithRoot; }; }, + getPublicApi() { + if (!publicSchema) { + throw new Error('First of all setup the publicSchema'); + } + + return publicSchema; + }, registerGetLayoutConfig(fn: GetLayoutConfig) { if (getLayoutConfig) { throw new Error( diff --git a/src/server/types/controllers.ts b/src/server/types/controllers.ts index 4bd6a22479..fb706d8c22 100644 --- a/src/server/types/controllers.ts +++ b/src/server/types/controllers.ts @@ -21,4 +21,5 @@ export type BasicControllers = | 'navigate' | 'navigation' | 'api.deleteLock' - | 'schematic-gateway'; + | 'schematic-gateway' + | 'public-api'; diff --git a/src/server/types/public-api.ts b/src/server/types/public-api.ts new file mode 100644 index 0000000000..eb6ebfd86b --- /dev/null +++ b/src/server/types/public-api.ts @@ -0,0 +1,18 @@ +import type {Request, Response} from '@gravity-ui/expresskit'; +import type {ApiWithRoot, SchemasByScope} from '@gravity-ui/gateway'; + +import type {DatalensGatewaySchemas} from './gateway'; + +type HeadersType = Record; + +export type PublicApiRpcMap = Record< + string, + Record< + string, + { + resolve: (api: ApiWithRoot) => any; + headers?: (req: Request, headers: HeadersType) => HeadersType; + args?: (req: Request) => Promise | object; + } + > +>; diff --git a/src/server/utils/index.ts b/src/server/utils/index.ts index 8a657de72f..1bc5d70ac4 100644 --- a/src/server/utils/index.ts +++ b/src/server/utils/index.ts @@ -84,6 +84,13 @@ class Utils { }; } + static pickRpcHeaders(req: Request) { + return { + ...pick(req.headers, [AuthHeader.Authorization]), + ...Utils.pickForwardHeaders(req.headers), + }; + } + static pickUsMasterToken(req: Request) { const token = req.headers[US_MASTER_TOKEN_HEADER]; if (typeof token !== 'string') { diff --git a/src/server/utils/routes.ts b/src/server/utils/routes.ts index 00581962ba..80eea2faeb 100644 --- a/src/server/utils/routes.ts +++ b/src/server/utils/routes.ts @@ -3,6 +3,7 @@ import { dlMainController, navigateController, navigationController, + publicApiControllerGetter, } from '../controllers'; import {registry} from '../registry'; import type {BasicControllers, ExtendedAppRouteDescription} from '../types/controllers'; @@ -40,6 +41,12 @@ export const getConfiguredRoute = ( ...params, }; } + case 'public-api': { + return { + handler: publicApiControllerGetter(undefined, params), + ...params, + }; + } default: return null as never; } diff --git a/src/shared/components/api-docs/index.ts b/src/shared/components/api-docs/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/shared/schema/bi/actions/datasets.ts b/src/shared/schema/bi/actions/datasets.ts index d185300a4a..49b6d4b58e 100644 --- a/src/shared/schema/bi/actions/datasets.ts +++ b/src/shared/schema/bi/actions/datasets.ts @@ -1,10 +1,13 @@ +import z from 'zod/v4'; + import { TIMEOUT_60_SEC, TIMEOUT_95_SEC, US_MASTER_TOKEN_HEADER, WORKBOOK_ID_HEADER, } from '../../../constants'; -import {createAction} from '../../gateway-utils'; +import {datasetSchema} from '../../../sdk/zod-shemas/dataset-api.schema'; +import {createAction, createTypedAction} from '../../gateway-utils'; import {filterUrlFragment} from '../../utils'; import { prepareDatasetProperty, @@ -72,6 +75,23 @@ export const actions = { headers: {...(workbookId ? {[WORKBOOK_ID_HEADER]: workbookId} : {}), ...headers}, }), }), + getDataset: createTypedAction({ + bodySchema: datasetSchema, + argsSchema: z.object({ + datasetId: z.string(), + version: z.literal('draft'), + workbookId: z.union([z.null(), z.string()]), + }), + })({ + method: 'GET', + path: ({datasetId, version}) => + `${API_V1}/datasets/${filterUrlFragment(datasetId)}/versions/${filterUrlFragment( + version, + )}`, + params: ({workbookId}, headers) => ({ + headers: {...(workbookId ? {[WORKBOOK_ID_HEADER]: workbookId} : {}), ...headers}, + }), + }), getFieldTypes: createAction({ method: 'GET', path: () => `${API_V1}/info/field_types`, diff --git a/src/shared/schema/gateway-utils.ts b/src/shared/schema/gateway-utils.ts index e0d795fc75..565fecb171 100644 --- a/src/shared/schema/gateway-utils.ts +++ b/src/shared/schema/gateway-utils.ts @@ -1,6 +1,12 @@ import type {Request, Response} from '@gravity-ui/expresskit'; -import type {ApiServiceActionConfig, GetAuthHeaders} from '@gravity-ui/gateway'; +import type { + ApiServiceActionConfig, + ApiServiceMixedActionConfig, + ApiServiceRestActionConfig, + GetAuthHeaders, +} from '@gravity-ui/gateway'; import type {AppContext} from '@gravity-ui/nodekit'; +import z from 'zod/v4'; import {AuthHeader, SERVICE_USER_ACCESS_TOKEN_HEADER} from '../constants'; @@ -12,6 +18,119 @@ export function createAction, +): actionConfig is ApiServiceRestActionConfig { + return Boolean((actionConfig as ApiServiceRestActionConfig).method); +} + +function isMixedActionConfig( + actionConfig: ApiServiceActionConfig, +): actionConfig is ApiServiceMixedActionConfig { + return typeof actionConfig === 'function'; +} + +const VALIDATION_SCHEMA_KEY = Symbol('$schema'); + +export const registerValidationSchema = >( + actionConfig: T, + schema: any, +) => { + Object.defineProperty(actionConfig, VALIDATION_SCHEMA_KEY, { + value: schema, + enumerable: false, + }); + + return actionConfig; +}; + +export const hasValidationSchema = >( + actionConfig: T, +) => { + return Object.hasOwnProperty.call(actionConfig, VALIDATION_SCHEMA_KEY); +}; + +export const getValidationSchema = >( + actionConfig: T, +) => { + return hasValidationSchema(actionConfig) ? (actionConfig as any)[VALIDATION_SCHEMA_KEY] : null; +}; + +const CONTENT_TYPE_JSON = 'application/json'; + +export function createTypedAction< + TOutputSchema extends z.ZodType, + TParamsSchema extends z.ZodType, + TTransformedSchema extends z.ZodType = TOutputSchema, + TOutput = z.infer, + TParams = z.infer, + TTransformed = z.infer, +>(schema: {bodySchema: TOutputSchema; argsSchema: TParamsSchema}) { + type ActionConfig = ApiServiceActionConfig< + AppContext, + Request, + Response, + TOutput, + TParams, + TTransformed + >; + + const shemaValidationObject = { + getSchema() { + return schema; + }, + getOpenApichema() { + return { + summary: 'Action summary', + // tags: [ApiTag.Workbooks], + request: { + body: { + content: { + [CONTENT_TYPE_JSON]: { + schema: z.toJSONSchema(schema.argsSchema), + }, + }, + }, + }, + responses: { + 200: { + description: 'Response', + content: { + [CONTENT_TYPE_JSON]: { + schema: z.toJSONSchema(schema.bodySchema), + }, + }, + }, + }, + }; + }, + }; + + const action = (actionConfig: ActionConfig) => + registerValidationSchema(actionConfig, shemaValidationObject); + + action.withValidationSchema = (actionConfig: ActionConfig) => { + if (isRestActionConfig(actionConfig)) { + return registerValidationSchema( + { + ...actionConfig, + validationSchema: z.toJSONSchema(schema.argsSchema, { + target: 'draft-7', + io: 'input', + }), + }, + shemaValidationObject, + ); + } else if (isMixedActionConfig(actionConfig)) { + return action(actionConfig); // TODO add validation + } + + return action(actionConfig); + }; + + return action; +} + type AuthArgsData = { userAccessToken?: string; serviceUserAccessToken?: string; diff --git a/src/shared/sdk/zod-shemas/dataset-api.schema.ts b/src/shared/sdk/zod-shemas/dataset-api.schema.ts index bd38e32160..28ddbdb126 100644 --- a/src/shared/sdk/zod-shemas/dataset-api.schema.ts +++ b/src/shared/sdk/zod-shemas/dataset-api.schema.ts @@ -272,7 +272,7 @@ const datasetApiErrorSchema = z.object({ const datasetSelectionMapSchema = z.record(z.string(), z.literal(true)); // Main Dataset schema -const datasetSchema = z.object({ +export const datasetSchema = z.object({ id: z.string(), realName: z.string(), is_favorite: z.boolean(), From 9f50ea30b571d8c78e27e28b0dcdaede69cdd7d1 Mon Sep 17 00:00:00 2001 From: Serge Pavlyuk Date: Thu, 31 Jul 2025 16:39:13 +0300 Subject: [PATCH 03/40] Swagger --- src/server/components/app-docs/index.ts | 42 ++++++ src/server/components/sdk/dash.ts | 9 +- src/server/controllers/public-api.ts | 130 ++++++++++++++++-- src/server/expresskit.ts | 7 +- src/server/types/public-api.ts | 4 + src/shared/schema/bi/actions/datasets.ts | 95 ++++++++++--- src/shared/schema/gateway-utils.ts | 6 +- src/shared/schema/mix/actions/dash.ts | 113 ++++++++++++++- src/shared/sdk/zod-shemas/dash-api.schema.ts | 9 +- .../sdk/zod-shemas/dataset-api.schema.ts | 48 +++---- 10 files changed, 403 insertions(+), 60 deletions(-) create mode 100644 src/server/components/app-docs/index.ts diff --git a/src/server/components/app-docs/index.ts b/src/server/components/app-docs/index.ts new file mode 100644 index 0000000000..43cc8a4305 --- /dev/null +++ b/src/server/components/app-docs/index.ts @@ -0,0 +1,42 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import {OpenAPIRegistry, OpenApiGeneratorV31} from '@asteasolutions/zod-to-openapi'; +import type {ExpressKit} from '@gravity-ui/expresskit'; +// eslint-disable-next-line import/no-extraneous-dependencies +import swaggerUi from 'swagger-ui-express'; + +export const openApiRegistry = new OpenAPIRegistry(); + +export const initSwagger = ( + app: ExpressKit, + // securitySchemes?: GetAdditionalSecuritySchemesResult, +) => { + const {config} = app; + + const installationText = `Installation – ${config.appInstallation}`; + const envText = `Env – ${config.appEnv}`; + const descriptionText = `
Datalens api.`; + + setImmediate(() => { + openApiRegistry.registerComponent('securitySchemes', 'Access token', { + type: 'apiKey', + in: 'header', + name: 'Authorization', + }); + + app.express.use( + '/api-docs/', + swaggerUi.serve, + swaggerUi.setup( + new OpenApiGeneratorV31(openApiRegistry.definitions).generateDocument({ + openapi: '3.1.0', + info: { + version: `${config.appVersion}`, + title: `UI API `, + description: [installationText, envText, descriptionText].join('
'), + }, + servers: [{url: '/'}], + }), + ), + ); + }); +}; diff --git a/src/server/components/sdk/dash.ts b/src/server/components/sdk/dash.ts index f97139b94a..e4e02f87a2 100644 --- a/src/server/components/sdk/dash.ts +++ b/src/server/components/sdk/dash.ts @@ -283,6 +283,7 @@ class Dash { params: EntryReadParams | null, headers: IncomingHttpHeaders, ctx: AppContext, + options?: {forceMigrate?: boolean}, ): Promise { try { const headersWithMetadata = { @@ -300,7 +301,10 @@ class Dash { const isServerMigrationEnabled = Boolean( isEnabledServerFeature(Feature.DashServerMigrationEnable), ); - if (isServerMigrationEnabled && DashSchemeConverter.isUpdateNeeded(result.data)) { + if ( + (options?.forceMigrate || isServerMigrationEnabled) && + DashSchemeConverter.isUpdateNeeded(result.data) + ) { result.data = await Dash.migrate(result.data); } @@ -324,6 +328,7 @@ class Dash { headers: IncomingHttpHeaders, ctx: AppContext, I18n: ServerI18n, + options?: {forceMigrate?: boolean}, ): Promise { try { const usData: typeof data & {skipSyncLinks?: boolean} = {...data}; @@ -332,7 +337,7 @@ class Dash { const needDataSend = !(mode === EntryUpdateMode.Publish && data.revId); if (needDataSend) { if (needSetDefaultData(usData.data)) { - const initialData = await Dash.read(entryId, null, headers, ctx); + const initialData = await Dash.read(entryId, null, headers, ctx, options); usData.data = setDefaultData(I18n, usData.data, initialData.data); } diff --git a/src/server/controllers/public-api.ts b/src/server/controllers/public-api.ts index 32dab15247..eddc89870c 100644 --- a/src/server/controllers/public-api.ts +++ b/src/server/controllers/public-api.ts @@ -3,6 +3,7 @@ import {REQUEST_ID_PARAM_NAME} from '@gravity-ui/nodekit'; import _ from 'lodash'; import {getValidationSchema, hasValidationSchema} from '../../shared/schema/gateway-utils'; +import {openApiRegistry} from '../components/app-docs'; import {PUBLIC_API_RPC_ERROR_CODE} from '../constants/public-api'; import {registry} from '../registry'; import type {DatalensGatewaySchemas} from '../types/gateway'; @@ -11,18 +12,122 @@ import Utils from '../utils'; const proxyMap: PublicApiRpcMap = { v0: { + // dataset getDataset: { - resolve: (api) => api.bi.getDataset, + resolve: (api) => api.bi.getDatasetApi, + openApi: { + summary: 'Get dataset', + tags: ['dataset'], + }, }, updateDataset: { - resolve: (api) => api.bi.updateDataset, + resolve: (api) => api.bi.updateDatasetApi, + openApi: { + summary: 'Update dataset', + tags: ['dataset'], + }, }, createDataset: { - resolve: (api) => api.bi.createDataset, + resolve: (api) => api.bi.createDatasetApi, + openApi: { + summary: 'Create dataset', + tags: ['dataset'], + }, }, deleteDataset: { + resolve: (api) => api.bi.deleteDatasetApi, + openApi: { + summary: 'Delete dataset', + tags: ['dataset'], + }, + }, + // wizard + getWizardChart: { + resolve: (api) => api.bi.createDataset, + openApi: { + summary: 'Get wizard chart', + tags: ['wizard'], + }, + }, + updateWizardChart: { + resolve: (api) => api.bi.updateDataset, + openApi: { + summary: 'Delete wizard chart', + tags: ['wizard'], + }, + }, + createWizardChart: { + resolve: (api) => api.bi.createDataset, + openApi: { + summary: 'Create wizard chart', + tags: ['wizard'], + }, + }, + deleteWizardChart: { resolve: (api) => api.bi.deleteDataset, + openApi: { + summary: 'Delete wizard chart', + tags: ['wizard'], + }, + }, + // Dash + getDashboard: { + resolve: (api) => api.mix.getDashApi, + openApi: { + summary: 'Get dashboard', + tags: ['dashboard'], + }, + }, + updateDashboard: { + resolve: (api) => api.mix.updateDashboardApi, + openApi: { + summary: 'Delete dashboard', + tags: ['dashboard'], + }, }, + createDashboard: { + resolve: (api) => api.mix.updateDashboardApi, + openApi: { + summary: 'Create dashboard', + tags: ['dashboard'], + }, + }, + deleteDashboard: { + resolve: (api) => api.mix.deleteDashboardApi, + openApi: { + summary: 'Delete dashboard', + tags: ['dashboard'], + }, + }, + // Report + // getReport: { + // resolve: (api) => api.bi.createDataset, + // openApi: { + // summary: 'Get report', + // tags: ['report'], + // }, + // }, + // updateReport: { + // resolve: (api) => api.bi.updateDataset, + // openApi: { + // summary: 'Delete report', + // tags: ['report'], + // }, + // }, + // createReport: { + // resolve: (api) => api.bi.createDataset, + // openApi: { + // summary: 'Create report', + // tags: ['report'], + // }, + // }, + // deleteReport: { + // resolve: (api) => api.bi.deleteDataset, + // openApi: { + // summary: 'Delete report', + // tags: ['report'], + // }, + // }, }, }; @@ -57,12 +162,17 @@ export function publicApiControllerGetter( const {gatewayApi} = registry.getGatewayApi(); Object.entries(gatewayProxyMap).forEach(([version, actions]) => { - Object.entries(actions).forEach(([action, {resolve}]) => { - if (hasValidationSchema(resolve(gatewayApi))) { - console.log( - parsedRoute.reverse({version, action}), - getValidationSchema(resolve(gatewayApi)).getOpenApichema(), - ); + Object.entries(actions).forEach(([action, {resolve, openApi}]) => { + const gatewayApiAction = resolve(gatewayApi); + + if (hasValidationSchema(gatewayApiAction)) { + openApiRegistry.registerPath({ + method: parsedRoute.method.toLocaleLowerCase(), + path: parsedRoute.reverse({version, action}), + ...openApi, + ...getValidationSchema(gatewayApiAction)().getOpenApichema(), + security: [{['Access token']: []}], + }); } }); }); @@ -101,7 +211,7 @@ export function publicApiControllerGetter( requestId, }); - res.status(200).send(result); + res.status(200).send(result.responseData); } catch (err) { const {error} = err as any; if (error) { diff --git a/src/server/expresskit.ts b/src/server/expresskit.ts index b903955a40..6e9cb86b56 100644 --- a/src/server/expresskit.ts +++ b/src/server/expresskit.ts @@ -2,6 +2,7 @@ import type {AppRoutes} from '@gravity-ui/expresskit'; import {ExpressKit} from '@gravity-ui/expresskit'; import type {NodeKit} from '@gravity-ui/nodekit'; +import {initSwagger} from './components/app-docs'; import type {ExtendedAppRouteDescription} from './types/controllers'; export function getExpressKit({ @@ -20,5 +21,9 @@ export function getExpressKit({ routes[route] = params; }); - return new ExpressKit(nodekit, routes); + const app = new ExpressKit(nodekit, routes); + + initSwagger(app); + + return app; } diff --git a/src/server/types/public-api.ts b/src/server/types/public-api.ts index eb6ebfd86b..a654cd2e78 100644 --- a/src/server/types/public-api.ts +++ b/src/server/types/public-api.ts @@ -13,6 +13,10 @@ export type PublicApiRpcMap) => any; headers?: (req: Request, headers: HeadersType) => HeadersType; args?: (req: Request) => Promise | object; + openApi: { + summary: string; + tags?: string[]; + }; } > >; diff --git a/src/shared/schema/bi/actions/datasets.ts b/src/shared/schema/bi/actions/datasets.ts index 49b6d4b58e..70f91db330 100644 --- a/src/shared/schema/bi/actions/datasets.ts +++ b/src/shared/schema/bi/actions/datasets.ts @@ -6,7 +6,11 @@ import { US_MASTER_TOKEN_HEADER, WORKBOOK_ID_HEADER, } from '../../../constants'; -import {datasetSchema} from '../../../sdk/zod-shemas/dataset-api.schema'; +import { + datasetBodySchema, + datasetOptionsSchema, + datasetSchema, +} from '../../../sdk/zod-shemas/dataset-api.schema'; import {createAction, createTypedAction} from '../../gateway-utils'; import {filterUrlFragment} from '../../utils'; import { @@ -75,23 +79,6 @@ export const actions = { headers: {...(workbookId ? {[WORKBOOK_ID_HEADER]: workbookId} : {}), ...headers}, }), }), - getDataset: createTypedAction({ - bodySchema: datasetSchema, - argsSchema: z.object({ - datasetId: z.string(), - version: z.literal('draft'), - workbookId: z.union([z.null(), z.string()]), - }), - })({ - method: 'GET', - path: ({datasetId, version}) => - `${API_V1}/datasets/${filterUrlFragment(datasetId)}/versions/${filterUrlFragment( - version, - )}`, - params: ({workbookId}, headers) => ({ - headers: {...(workbookId ? {[WORKBOOK_ID_HEADER]: workbookId} : {}), ...headers}, - }), - }), getFieldTypes: createAction({ method: 'GET', path: () => `${API_V1}/info/field_types`, @@ -271,6 +258,78 @@ export const actions = { path: ({datasetId}) => `${API_V1}/datasets/${filterUrlFragment(datasetId)}`, params: (_, headers) => ({headers}), }), + createDatasetApi: createTypedAction({ + bodySchema: z.object({ + id: z.string(), + dataset: datasetBodySchema, + options: datasetOptionsSchema, + }), + argsSchema: z.object({ + name: z.string(), + created_via: z.string().optional(), + multisource: z.boolean(), + dataset: datasetBodySchema, + dir_path: z.string().optional(), + workbook_id: z.string().optional(), + }), + }).withValidationSchema({ + method: 'POST', + path: () => `${API_V1}/datasets`, + params: ({dataset, ...restBody}, headers, {ctx}) => { + const resultDataset = prepareDatasetProperty(ctx, dataset); + return {body: {...restBody, dataset: resultDataset}, headers}; + }, + }), + updateDatasetApi: createTypedAction({ + bodySchema: z.object({ + id: z.string(), + dataset: datasetBodySchema, + options: datasetOptionsSchema, + }), + argsSchema: z.object({ + version: z.literal('draft'), + datasetId: z.string(), + multisource: z.boolean(), + dataset: datasetBodySchema, + }), + }).withValidationSchema({ + method: 'PUT', + path: ({datasetId, version}) => + `${API_V1}/datasets/${filterUrlFragment(datasetId)}/versions/${filterUrlFragment( + version, + )}`, + params: ({dataset, multisource}, headers, {ctx}) => { + const resultDataset = prepareDatasetProperty(ctx, dataset); + return {body: {dataset: resultDataset, multisource}, headers}; + }, + }), + deleteDatasetApi: createTypedAction({ + bodySchema: z.unknown(), + argsSchema: z.object({ + datasetId: z.string(), + }), + }).withValidationSchema({ + method: 'DELETE', + path: ({datasetId}) => `${API_V1}/datasets/${filterUrlFragment(datasetId)}`, + params: (_, headers) => ({headers}), + }), + getDatasetApi: createTypedAction({ + bodySchema: datasetSchema, + argsSchema: z.object({ + datasetId: z.string(), + version: z.literal('draft'), + workbookId: z.union([z.null(), z.string()]), + }), + }).withValidationSchema({ + method: 'GET', + path: ({datasetId, version}) => + `${API_V1}/datasets/${filterUrlFragment(datasetId)}/versions/${filterUrlFragment( + version, + )}`, + params: ({workbookId}, headers) => ({ + headers: {...(workbookId ? {[WORKBOOK_ID_HEADER]: workbookId} : {}), ...headers}, + }), + }), _proxyExportDataset: createAction({ method: 'POST', path: ({datasetId}) => `${API_V1}/datasets/export/${datasetId}`, diff --git a/src/shared/schema/gateway-utils.ts b/src/shared/schema/gateway-utils.ts index 565fecb171..361b5140ca 100644 --- a/src/shared/schema/gateway-utils.ts +++ b/src/shared/schema/gateway-utils.ts @@ -75,14 +75,12 @@ export function createTypedAction< TTransformed >; - const shemaValidationObject = { + const shemaValidationObject = () => ({ getSchema() { return schema; }, getOpenApichema() { return { - summary: 'Action summary', - // tags: [ApiTag.Workbooks], request: { body: { content: { @@ -104,7 +102,7 @@ export function createTypedAction< }, }; }, - }; + }); const action = (actionConfig: ActionConfig) => registerValidationSchema(actionConfig, shemaValidationObject); diff --git a/src/shared/schema/mix/actions/dash.ts b/src/shared/schema/mix/actions/dash.ts index 718d92eb53..d0dd45fb57 100644 --- a/src/shared/schema/mix/actions/dash.ts +++ b/src/shared/schema/mix/actions/dash.ts @@ -1,7 +1,13 @@ +import _, {pick} from 'lodash'; import type {DeepNonNullable} from 'utility-types'; +import {z} from 'zod/v4'; +import Dash from '../../../../server/components/sdk/dash'; +import {DASH_ENTRY_RELEVANT_FIELDS} from '../../../../server/constants'; +import {dashSchema} from '../../../sdk/zod-shemas/dash-api.schema'; import type {ChartsStats} from '../../../types/charts'; -import {createAction} from '../../gateway-utils'; +import {EntryScope} from '../../../types/common'; +import {createAction, createTypedAction} from '../../gateway-utils'; import {getTypedApi} from '../../simple-schema'; import {getEntryVisualizationType} from '../helpers'; import type {DatasetDictResponse, DatasetFieldsDictResponse} from '../helpers/dash'; @@ -22,7 +28,112 @@ import { type GetWidgetsDatasetsFieldsResponse, } from '../types'; +const dashUsSchema = z.object({ + ...dashSchema.shape, + entryId: z.string(), + scope: z.literal(EntryScope.Dash), + public: z.boolean(), + isFavorite: z.boolean(), + createdAt: z.string(), + createdBy: z.string(), + updatedAt: z.string(), + updatedBy: z.string(), + revId: z.string(), + savedId: z.string(), + publishedId: z.string(), + meta: z.record(z.string(), z.string()), + links: z.record(z.string(), z.string()), + key: z.union([z.null(), z.string()]), + workbookId: z.union([z.null(), z.string()]), + type: z.literal(''), +}); + +const dashUsCreateSchema = z.object({ + ...dashSchema.shape, + key: z.string().optional(), + workbookId: z.union([z.null(), z.string()]).optional(), + lockToken: z.string().optional(), + mode: z.literal(['publish', 'save']), +}); + +const dashUsUpdateSchema = z.object({ + ...dashSchema.partial().shape, + entryId: z.string(), +}); + export const dashActions = { + getDashApi: createTypedAction({ + argsSchema: z.object({ + dashboardId: z.string(), + revId: z.string().optional(), + includePermissions: z.boolean(), + includeLinks: z.boolean(), + branch: z.literal(['published', 'saved']).optional().default('published'), + }), + bodySchema: dashUsSchema, + }).withValidationSchema(async (_, args, {headers, ctx}) => { + const {dashboardId, includePermissions, includeLinks, branch, revId} = args; + + if (!dashboardId || dashboardId === 'null') { + throw new Error(`Not found ${dashboardId} id`); + } + + const result = await Dash.read( + dashboardId, + { + includePermissions: includePermissions?.toString(), + includeLinks: includeLinks?.toString(), + ...(branch ? {branch} : {}), + ...(revId ? {revId} : {}), + }, + headers, + ctx, + {forceMigrate: true}, + ); + + if (result.scope !== EntryScope.Dash) { + throw new Error('No entry found'); + } + + return pick(result, DASH_ENTRY_RELEVANT_FIELDS) as any; + }), + deleteDashboardApi: createTypedAction({ + argsSchema: z.object({ + dashboardId: z.string(), + lockToken: z.string().optional(), + }), + bodySchema: z.any(), + }).withValidationSchema(async (api, {lockToken, dashboardId}) => { + const typedApi = getTypedApi(api); + + await typedApi.us._deleteUSEntry({ + entryId: dashboardId, + lockToken, + }); + }), + updateDashboardApi: createTypedAction({ + argsSchema: dashUsUpdateSchema, + bodySchema: dashUsSchema, + }).withValidationSchema(async (_, args, {headers, ctx}) => { + const {entryId} = args; + + const I18n = ctx.get('i18n'); + + return (await Dash.update(entryId as any, args as any, headers, ctx, I18n, { + forceMigrate: true, + })) as unknown as z.infer; + }), + createDashboardApi: createTypedAction({ + argsSchema: dashUsCreateSchema, + bodySchema: dashUsSchema, + }).withValidationSchema(async (_, args, {headers, ctx}) => { + const I18n = ctx.get('i18n'); + + return (await Dash.create(args as any, headers, ctx, I18n)) as unknown as z.infer< + typeof dashUsSchema + >; + }), + collectDashStats: createAction( async (_, args, {ctx}) => { ctx.stats('dashStats', { diff --git a/src/shared/sdk/zod-shemas/dash-api.schema.ts b/src/shared/sdk/zod-shemas/dash-api.schema.ts index 0767628a86..223600118d 100644 --- a/src/shared/sdk/zod-shemas/dash-api.schema.ts +++ b/src/shared/sdk/zod-shemas/dash-api.schema.ts @@ -256,7 +256,13 @@ const settingsSchema = z.object({ const dataSchema = z.object({ counter: z.number().int().min(1).optional(), salt: z.string().min(1).optional(), - schemeVersion: z.number().int().min(1).max(DASH_CURRENT_SCHEME_VERSION).optional(), + schemeVersion: z + .number() + .int() + .min(1) + .max(DASH_CURRENT_SCHEME_VERSION) + .default(DASH_CURRENT_SCHEME_VERSION) + .optional(), tabs: z.array(tabSchema), settings: settingsSchema.optional(), }); @@ -264,6 +270,7 @@ const dataSchema = z.object({ // Main dashboard API validation schema export const dashSchema = z.object({ key: z.string().min(1).optional(), + workbookId: z.union([z.null(), z.string()]).optional(), data: dataSchema, meta: z.record(z.any(), z.any()).optional(), links: z.record(z.string(), z.string()).optional(), diff --git a/src/shared/sdk/zod-shemas/dataset-api.schema.ts b/src/shared/sdk/zod-shemas/dataset-api.schema.ts index 28ddbdb126..79926e1026 100644 --- a/src/shared/sdk/zod-shemas/dataset-api.schema.ts +++ b/src/shared/sdk/zod-shemas/dataset-api.schema.ts @@ -271,6 +271,30 @@ const datasetApiErrorSchema = z.object({ // Dataset selection map schema const datasetSelectionMapSchema = z.record(z.string(), z.literal(true)); +export const datasetBodySchema = z.object({ + avatar_relations: z.array(datasetAvatarRelationSchema), + component_errors: z.object({ + items: z.array(datasetComponentErrorSchema), + }), + obligatory_filters: z.array(obligatoryFilterSchema), + preview_enabled: z.boolean(), + result_schema: z.array(datasetFieldSchema), + result_schema_aux: z.object({ + inter_dependencies: z.object({ + deps: z.array(z.string()), + }), + }), + rls: datasetRlsSchema, + rls2: z.array(z.unknown()), + source_avatars: z.array(datasetSourceAvatarSchema), + source_features: z.record(z.string(), z.any()), + sources: z.array(datasetSourceSchema), + revisionId: z.string(), + load_preview_by_default: z.boolean(), + template_enabled: z.boolean(), + data_export_forbidden: z.boolean().optional(), +}); + // Main Dataset schema export const datasetSchema = z.object({ id: z.string(), @@ -278,29 +302,7 @@ export const datasetSchema = z.object({ is_favorite: z.boolean(), key: z.string(), options: datasetOptionsSchema, - dataset: z.object({ - avatar_relations: z.array(datasetAvatarRelationSchema), - component_errors: z.object({ - items: z.array(datasetComponentErrorSchema), - }), - obligatory_filters: z.array(obligatoryFilterSchema), - preview_enabled: z.boolean(), - result_schema: z.array(datasetFieldSchema), - result_schema_aux: z.object({ - inter_dependencies: z.object({ - deps: z.array(z.string()), - }), - }), - rls: datasetRlsSchema, - rls2: z.array(z.unknown()), - source_avatars: z.array(datasetSourceAvatarSchema), - source_features: z.record(z.string(), z.any()), - sources: z.array(datasetSourceSchema), - revisionId: z.string(), - load_preview_by_default: z.boolean(), - template_enabled: z.boolean(), - data_export_forbidden: z.boolean().optional(), - }), + dataset: datasetBodySchema, workbook_id: z.string().optional(), permissions: z.any().optional(), // Using z.any() for Permissions type as it's complex From 0b20b131b8eb519447b4ab3e735992bceb96f841 Mon Sep 17 00:00:00 2001 From: Serge Pavlyuk Date: Mon, 4 Aug 2025 14:44:11 +0300 Subject: [PATCH 04/40] Add wizard api hanlers --- .../storage/united-storage/provider.ts | 24 ++++++++++ src/server/controllers/public-api.ts | 48 +++++++++++++++++-- src/shared/schema/bi/actions/datasets.ts | 21 ++++---- src/shared/schema/mix/actions/dash.ts | 15 +++--- src/shared/schema/mix/actions/index.ts | 2 + src/shared/schema/mix/actions/wizard.ts | 44 +++++++++++++++++ ...i.schema.ts => wizard-chart-api.schema.ts} | 2 +- 7 files changed, 135 insertions(+), 21 deletions(-) create mode 100644 src/shared/schema/mix/actions/wizard.ts rename src/shared/sdk/zod-shemas/{chart-api.schema.ts => wizard-chart-api.schema.ts} (99%) diff --git a/src/server/components/charts-engine/components/storage/united-storage/provider.ts b/src/server/components/charts-engine/components/storage/united-storage/provider.ts index b08b0b37a0..63d16f951c 100644 --- a/src/server/components/charts-engine/components/storage/united-storage/provider.ts +++ b/src/server/components/charts-engine/components/storage/united-storage/provider.ts @@ -21,6 +21,7 @@ import { TRACE_ID_HEADER, US_PUBLIC_API_TOKEN_HEADER, WORKBOOK_ID_HEADER, + mapChartsConfigToLatestVersion, } from '../../../../../../shared'; import {ErrorCode, TIMEOUT_10_SEC} from '../../../../../../shared/constants'; import {createErrorHandler} from '../../error-handler'; @@ -341,6 +342,29 @@ export class USProvider { }); } + static async retrivePrasedWizardChart( + ctx: AppContext, + props: { + id: string; + storageApiPath?: string; + extraAllowedHeaders?: string[]; + unreleased: boolean | string; + includeLinks?: boolean | string; + includePermissionsInfo?: boolean | string; + revId?: string; + headers: Request['headers']; + workbookId?: WorkbookId; + includeServicePlan?: boolean; + includeTenantFeatures?: boolean; + }, + ) { + const result = await USProvider.retrieveById(ctx, props); + + result.data.shared = mapChartsConfigToLatestVersion(JSON.parse(result.data.shared)); + + return result; + } + static retrieveByKey( ctx: AppContext, { diff --git a/src/server/controllers/public-api.ts b/src/server/controllers/public-api.ts index eddc89870c..b0e04a8735 100644 --- a/src/server/controllers/public-api.ts +++ b/src/server/controllers/public-api.ts @@ -1,6 +1,7 @@ import type {Request, Response} from '@gravity-ui/expresskit'; import {REQUEST_ID_PARAM_NAME} from '@gravity-ui/nodekit'; import _ from 'lodash'; +import z from 'zod/v4'; import {getValidationSchema, hasValidationSchema} from '../../shared/schema/gateway-utils'; import {openApiRegistry} from '../components/app-docs'; @@ -12,6 +13,14 @@ import Utils from '../utils'; const proxyMap: PublicApiRpcMap = { v0: { + // navigation + getNavigationList: { + resolve: (api) => api.mix.getNavigationList, + openApi: { + summary: 'Get navigation list', + tags: ['navigation'], + }, + }, // dataset getDataset: { resolve: (api) => api.bi.getDatasetApi, @@ -43,7 +52,7 @@ const proxyMap: PublicApiRpcMap = { }, // wizard getWizardChart: { - resolve: (api) => api.bi.createDataset, + resolve: (api) => api.mix.getWizardChartApi, openApi: { summary: 'Get wizard chart', tags: ['wizard'], @@ -64,7 +73,7 @@ const proxyMap: PublicApiRpcMap = { }, }, deleteWizardChart: { - resolve: (api) => api.bi.deleteDataset, + resolve: (api) => api.mix.deleteWizardChartApi, openApi: { summary: 'Delete wizard chart', tags: ['wizard'], @@ -72,7 +81,7 @@ const proxyMap: PublicApiRpcMap = { }, // Dash getDashboard: { - resolve: (api) => api.mix.getDashApi, + resolve: (api) => api.mix.getDashboardApi, openApi: { summary: 'Get dashboard', tags: ['dashboard'], @@ -86,7 +95,7 @@ const proxyMap: PublicApiRpcMap = { }, }, createDashboard: { - resolve: (api) => api.mix.updateDashboardApi, + resolve: (api) => api.mix.createDashboardApi, openApi: { summary: 'Create dashboard', tags: ['dashboard'], @@ -154,6 +163,29 @@ const parseRoute = (route: string) => { }; }; +const defaultSchema = { + summary: 'Type not defined', + request: { + body: { + content: { + ['application/json']: { + schema: z.toJSONSchema(z.any()), + }, + }, + }, + }, + responses: { + 200: { + description: 'TBD', + content: { + ['application/json']: { + schema: z.toJSONSchema(z.any()), + }, + }, + }, + }, +}; + export function publicApiControllerGetter( gatewayProxyMap: PublicApiRpcMap = proxyMap, params: any, @@ -173,6 +205,14 @@ export function publicApiControllerGetter( ...getValidationSchema(gatewayApiAction)().getOpenApichema(), security: [{['Access token']: []}], }); + } else { + openApiRegistry.registerPath({ + method: parsedRoute.method.toLocaleLowerCase(), + path: parsedRoute.reverse({version, action}), + ...openApi, + ...defaultSchema, + security: [{['Access token']: []}], + } as any); } }); }); diff --git a/src/shared/schema/bi/actions/datasets.ts b/src/shared/schema/bi/actions/datasets.ts index 70f91db330..82acad34b4 100644 --- a/src/shared/schema/bi/actions/datasets.ts +++ b/src/shared/schema/bi/actions/datasets.ts @@ -58,6 +58,18 @@ const API_V1 = '/api/v1'; const API_DATA_V1 = '/api/data/v1'; const API_DATA_V2 = '/api/data/v2'; +const createDatasetDefaultArgsSchema = z.object({ + name: z.string(), + created_via: z.string().optional(), + multisource: z.boolean(), + dataset: datasetBodySchema, +}); + +const createDatasetArgsSchema = z.union([ + z.object({...createDatasetDefaultArgsSchema.shape, dir_path: z.string()}), + z.object({...createDatasetDefaultArgsSchema.shape, workbook_id: z.string()}), +]); + export const actions = { getSources: createAction({ method: 'GET', @@ -264,14 +276,7 @@ export const actions = { dataset: datasetBodySchema, options: datasetOptionsSchema, }), - argsSchema: z.object({ - name: z.string(), - created_via: z.string().optional(), - multisource: z.boolean(), - dataset: datasetBodySchema, - dir_path: z.string().optional(), - workbook_id: z.string().optional(), - }), + argsSchema: createDatasetArgsSchema, }).withValidationSchema({ method: 'POST', path: () => `${API_V1}/datasets`, diff --git a/src/shared/schema/mix/actions/dash.ts b/src/shared/schema/mix/actions/dash.ts index d0dd45fb57..b6dfa53c58 100644 --- a/src/shared/schema/mix/actions/dash.ts +++ b/src/shared/schema/mix/actions/dash.ts @@ -42,7 +42,7 @@ const dashUsSchema = z.object({ savedId: z.string(), publishedId: z.string(), meta: z.record(z.string(), z.string()), - links: z.record(z.string(), z.string()), + links: z.record(z.string(), z.string()).optional(), key: z.union([z.null(), z.string()]), workbookId: z.union([z.null(), z.string()]), type: z.literal(''), @@ -50,7 +50,6 @@ const dashUsSchema = z.object({ const dashUsCreateSchema = z.object({ ...dashSchema.shape, - key: z.string().optional(), workbookId: z.union([z.null(), z.string()]).optional(), lockToken: z.string().optional(), mode: z.literal(['publish', 'save']), @@ -62,12 +61,12 @@ const dashUsUpdateSchema = z.object({ }); export const dashActions = { - getDashApi: createTypedAction({ + getDashboardApi: createTypedAction({ argsSchema: z.object({ dashboardId: z.string(), revId: z.string().optional(), - includePermissions: z.boolean(), - includeLinks: z.boolean(), + includePermissions: z.boolean().optional().default(false), + includeLinks: z.boolean().optional().default(false), branch: z.literal(['published', 'saved']).optional().default('published'), }), bodySchema: dashUsSchema, @@ -81,9 +80,9 @@ export const dashActions = { const result = await Dash.read( dashboardId, { - includePermissions: includePermissions?.toString(), - includeLinks: includeLinks?.toString(), - ...(branch ? {branch} : {}), + includePermissions: includePermissions ? includePermissions?.toString() : '0', + includeLinks: includeLinks ? includeLinks?.toString() : '0', + ...(branch ? {branch} : {branch: 'published'}), ...(revId ? {revId} : {}), }, headers, diff --git a/src/shared/schema/mix/actions/index.ts b/src/shared/schema/mix/actions/index.ts index 1c24244896..d4bdfdca5b 100644 --- a/src/shared/schema/mix/actions/index.ts +++ b/src/shared/schema/mix/actions/index.ts @@ -3,6 +3,7 @@ import {editorActions} from './editor'; import {entriesActions} from './entries'; import {markdownActions} from './markdown'; import {navigationActions} from './navigation'; +import {wizardActions} from './wizard'; export const actions = { ...navigationActions, @@ -10,4 +11,5 @@ export const actions = { ...markdownActions, ...dashActions, ...editorActions, + ...wizardActions, }; diff --git a/src/shared/schema/mix/actions/wizard.ts b/src/shared/schema/mix/actions/wizard.ts new file mode 100644 index 0000000000..bb0230238c --- /dev/null +++ b/src/shared/schema/mix/actions/wizard.ts @@ -0,0 +1,44 @@ +import z from 'zod/v4'; + +import {USProvider} from '../../../../server/components/charts-engine/components/storage/united-storage/provider'; +import {v12ChartsConfigSchema} from '../../../sdk/zod-shemas/wizard-chart-api.schema'; +import {createTypedAction} from '../../gateway-utils'; +import {getTypedApi} from '../../simple-schema'; + +export const wizardActions = { + getWizardChartApi: createTypedAction({ + argsSchema: z.object({ + chardId: z.string(), + unreleased: z.boolean().default(false).optional(), + revId: z.string().optional(), + includePermissions: z.boolean().default(false).optional(), + includeLinks: z.boolean().default(false).optional(), + }), + bodySchema: v12ChartsConfigSchema, + }).withValidationSchema(async (_, args, {ctx, headers}) => { + const {includePermissions, includeLinks, unreleased, revId, chardId} = args; + + const result = await USProvider.retrivePrasedWizardChart(ctx, { + id: chardId, + includePermissionsInfo: includePermissions ? includePermissions?.toString() : '0', + includeLinks: includeLinks ? includeLinks?.toString() : '0', + ...(revId ? {revId} : {}), + ...(unreleased ? {unreleased} : {unreleased: false}), + headers, + }); + + return result as any; + }), + deleteWizardChartApi: createTypedAction({ + argsSchema: z.object({ + chartId: z.string(), + }), + bodySchema: z.any(), + }).withValidationSchema(async (api, {chartId}) => { + const typedApi = getTypedApi(api); + + await typedApi.us._deleteUSEntry({ + entryId: chartId, + }); + }), +}; diff --git a/src/shared/sdk/zod-shemas/chart-api.schema.ts b/src/shared/sdk/zod-shemas/wizard-chart-api.schema.ts similarity index 99% rename from src/shared/sdk/zod-shemas/chart-api.schema.ts rename to src/shared/sdk/zod-shemas/wizard-chart-api.schema.ts index f74f8f9c3e..1d71632131 100644 --- a/src/shared/sdk/zod-shemas/chart-api.schema.ts +++ b/src/shared/sdk/zod-shemas/wizard-chart-api.schema.ts @@ -7,7 +7,7 @@ import { LabelsPositions, MapCenterMode, ZoomMode, -} from '../../'; +} from '../..'; import {WidgetSize} from '../../constants'; import {MARKUP_TYPE} from '../../types/charts'; import type {DatasetFieldCalcMode} from '../../types/dataset'; From d6a46807794b5c41d29e60be08e417a60a3f5be9 Mon Sep 17 00:00:00 2001 From: Serge Pavlyuk Date: Mon, 4 Aug 2025 16:47:52 +0300 Subject: [PATCH 05/40] Wizard Hierarchy fix --- src/shared/schema/mix/actions/wizard.ts | 25 +++++++++- .../sdk/zod-shemas/wizard-chart-api.schema.ts | 49 ++++++++++--------- 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/src/shared/schema/mix/actions/wizard.ts b/src/shared/schema/mix/actions/wizard.ts index bb0230238c..d6d5fb3697 100644 --- a/src/shared/schema/mix/actions/wizard.ts +++ b/src/shared/schema/mix/actions/wizard.ts @@ -2,9 +2,32 @@ import z from 'zod/v4'; import {USProvider} from '../../../../server/components/charts-engine/components/storage/united-storage/provider'; import {v12ChartsConfigSchema} from '../../../sdk/zod-shemas/wizard-chart-api.schema'; +import {EntryScope, WizardType} from '../../../types'; import {createTypedAction} from '../../gateway-utils'; import {getTypedApi} from '../../simple-schema'; +const wizardUsSchema = z.object({ + data: z.object({ + shared: v12ChartsConfigSchema, + }), + entryId: z.string(), + scope: z.literal(EntryScope.Widget), + type: z.enum(WizardType), + public: z.boolean(), + isFavorite: z.boolean(), + createdAt: z.string(), + createdBy: z.string(), + updatedAt: z.string(), + updatedBy: z.string(), + revId: z.string(), + savedId: z.string(), + publishedId: z.string(), + meta: z.record(z.string(), z.string()), + links: z.record(z.string(), z.string()).optional(), + key: z.union([z.null(), z.string()]), + workbookId: z.union([z.null(), z.string()]), +}); + export const wizardActions = { getWizardChartApi: createTypedAction({ argsSchema: z.object({ @@ -14,7 +37,7 @@ export const wizardActions = { includePermissions: z.boolean().default(false).optional(), includeLinks: z.boolean().default(false).optional(), }), - bodySchema: v12ChartsConfigSchema, + bodySchema: wizardUsSchema, }).withValidationSchema(async (_, args, {ctx, headers}) => { const {includePermissions, includeLinks, unreleased, revId, chardId} = args; diff --git a/src/shared/sdk/zod-shemas/wizard-chart-api.schema.ts b/src/shared/sdk/zod-shemas/wizard-chart-api.schema.ts index 1d71632131..ea3370d78e 100644 --- a/src/shared/sdk/zod-shemas/wizard-chart-api.schema.ts +++ b/src/shared/sdk/zod-shemas/wizard-chart-api.schema.ts @@ -256,30 +256,31 @@ const v12PlaceholderSettingsSchema = z.object({ }); // Forward declaration for recursive types -const v12FieldSchema: z.ZodType = z.lazy(() => - z - .object({ - data_type: z.string(), - fields: z.array(v12FieldSchema).optional(), - type: z.string(), - title: z.string(), - guid: z.string(), - formatting: v12FormattingSchema.optional(), - format: z.string().optional(), - datasetId: z.string(), - source: z.string().optional(), - datasetName: z.string().optional(), - hideLabelMode: z.string().optional(), - calc_mode: datasetFieldCalcModeSchema, - default_value: parameterDefaultValueSchema.optional(), - barsSettings: z.any().optional(), // TableBarsSettings - subTotalsSettings: z.any().optional(), // TableSubTotalsSettings - backgroundSettings: z.any().optional(), // TableFieldBackgroundSettings - columnSettings: z.any().optional(), // ColumnSettings - hintSettings: z.any().optional(), // HintSettings - }) - .merge(v12ClientOnlyFieldsSchema), -); +const v12FieldSchemaInner = z.object({ + ...v12ClientOnlyFieldsSchema.shape, + data_type: z.string(), + type: z.string(), + title: z.string(), + guid: z.string(), + formatting: v12FormattingSchema.optional(), + format: z.string().optional(), + datasetId: z.string(), + source: z.string().optional(), + datasetName: z.string().optional(), + hideLabelMode: z.string().optional(), + calc_mode: datasetFieldCalcModeSchema, + default_value: parameterDefaultValueSchema.optional(), + barsSettings: z.any().optional(), // TableBarsSettings + subTotalsSettings: z.any().optional(), // TableSubTotalsSettings + backgroundSettings: z.any().optional(), // TableFieldBackgroundSettings + columnSettings: z.any().optional(), // ColumnSettings + hintSettings: z.any().optional(), // HintSettings +}); + +const v12FieldSchema = z.object({ + ...v12FieldSchemaInner.shape, + fields: z.array(v12FieldSchemaInner).optional(), +}); // V12Placeholder schema const v12PlaceholderSchema = z.object({ From c7eb29994841115f53ab0af8b06564bf737d6673 Mon Sep 17 00:00:00 2001 From: Serge Pavlyuk Date: Mon, 4 Aug 2025 17:15:03 +0300 Subject: [PATCH 06/40] Wizard update and create --- .../storage/united-storage/provider.ts | 6 +-- src/server/controllers/public-api.ts | 4 +- src/shared/schema/mix/actions/wizard.ts | 50 +++++++++++++++++-- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/server/components/charts-engine/components/storage/united-storage/provider.ts b/src/server/components/charts-engine/components/storage/united-storage/provider.ts index 63d16f951c..d209d9d9a6 100644 --- a/src/server/components/charts-engine/components/storage/united-storage/provider.ts +++ b/src/server/components/charts-engine/components/storage/united-storage/provider.ts @@ -216,7 +216,7 @@ export type ProviderCreateParams = { recursion?: boolean; meta?: Record; includePermissionsInfo?: boolean | string; - workbookId: string; + workbookId: string | null; name: string; mode?: EntryUpdateMode; }; @@ -360,7 +360,7 @@ export class USProvider { ) { const result = await USProvider.retrieveById(ctx, props); - result.data.shared = mapChartsConfigToLatestVersion(JSON.parse(result.data.shared)); + result.data = mapChartsConfigToLatestVersion(JSON.parse(result.data.shared)); return result; } @@ -615,7 +615,7 @@ export class USProvider { recursion: boolean; links?: unknown; meta: Record; - workbookId: string; + workbookId: string | null; name: string; includePermissionsInfo?: boolean; mode: EntryUpdateMode; diff --git a/src/server/controllers/public-api.ts b/src/server/controllers/public-api.ts index b0e04a8735..b5a0332899 100644 --- a/src/server/controllers/public-api.ts +++ b/src/server/controllers/public-api.ts @@ -59,14 +59,14 @@ const proxyMap: PublicApiRpcMap = { }, }, updateWizardChart: { - resolve: (api) => api.bi.updateDataset, + resolve: (api) => api.mix.updateWizardChartApi, openApi: { summary: 'Delete wizard chart', tags: ['wizard'], }, }, createWizardChart: { - resolve: (api) => api.bi.createDataset, + resolve: (api) => api.mix.createWizardChartApi, openApi: { summary: 'Create wizard chart', tags: ['wizard'], diff --git a/src/shared/schema/mix/actions/wizard.ts b/src/shared/schema/mix/actions/wizard.ts index d6d5fb3697..9ff736b352 100644 --- a/src/shared/schema/mix/actions/wizard.ts +++ b/src/shared/schema/mix/actions/wizard.ts @@ -7,9 +7,7 @@ import {createTypedAction} from '../../gateway-utils'; import {getTypedApi} from '../../simple-schema'; const wizardUsSchema = z.object({ - data: z.object({ - shared: v12ChartsConfigSchema, - }), + data: v12ChartsConfigSchema, entryId: z.string(), scope: z.literal(EntryScope.Widget), type: z.enum(WizardType), @@ -52,6 +50,52 @@ export const wizardActions = { return result as any; }), + createWizardChartApi: createTypedAction({ + argsSchema: z.object({ + entryId: z.string(), + data: v12ChartsConfigSchema, + key: z.string(), + workbookId: z.union([z.string(), z.null()]).optional(), + type: z.enum(WizardType).optional(), + name: z.string(), + }), + bodySchema: wizardUsSchema, + }).withValidationSchema(async (_, args, {ctx, headers}) => { + const {data, type, key, workbookId, name} = args; + + const result = await USProvider.create(ctx, { + type, + data, + key, + name, + scope: EntryScope.Widget, + ...(workbookId ? {workbookId} : {workbookId: null}), + headers, + }); + + return result as any; + }), + updateWizardChartApi: createTypedAction({ + argsSchema: z.object({ + entryId: z.string(), + revId: z.string().optional(), + data: v12ChartsConfigSchema, + type: z.enum(WizardType).optional(), + }), + bodySchema: wizardUsSchema, + }).withValidationSchema(async (_, args, {ctx, headers}) => { + const {entryId, revId, data, type} = args; + + const result = await USProvider.update(ctx, { + entryId, + ...(revId ? {revId} : {}), + ...(type ? {type} : {}), + data, + headers, + }); + + return result as any; + }), deleteWizardChartApi: createTypedAction({ argsSchema: z.object({ chartId: z.string(), From 08a08a147c429746008adb91477540be0b7fb735 Mon Sep 17 00:00:00 2001 From: Serge Pavlyuk Date: Mon, 4 Aug 2025 17:38:57 +0300 Subject: [PATCH 07/40] Fix types and comment --- .../charts-engine/components/storage/united-storage/provider.ts | 2 +- src/server/controllers/public-api.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/components/charts-engine/components/storage/united-storage/provider.ts b/src/server/components/charts-engine/components/storage/united-storage/provider.ts index d209d9d9a6..dd34409b2e 100644 --- a/src/server/components/charts-engine/components/storage/united-storage/provider.ts +++ b/src/server/components/charts-engine/components/storage/united-storage/provider.ts @@ -360,7 +360,7 @@ export class USProvider { ) { const result = await USProvider.retrieveById(ctx, props); - result.data = mapChartsConfigToLatestVersion(JSON.parse(result.data.shared)); + result.data = mapChartsConfigToLatestVersion(JSON.parse(result.data.shared)) as any; return result; } diff --git a/src/server/controllers/public-api.ts b/src/server/controllers/public-api.ts index b5a0332899..d83450a91e 100644 --- a/src/server/controllers/public-api.ts +++ b/src/server/controllers/public-api.ts @@ -61,7 +61,7 @@ const proxyMap: PublicApiRpcMap = { updateWizardChart: { resolve: (api) => api.mix.updateWizardChartApi, openApi: { - summary: 'Delete wizard chart', + summary: 'Update wizard chart', tags: ['wizard'], }, }, From 5356131bf703442fe01db662ff6b63a27e77515c Mon Sep 17 00:00:00 2001 From: Serge Pavlyuk Date: Wed, 6 Aug 2025 16:30:24 +0300 Subject: [PATCH 08/40] Added actions --- src/server/controllers/public-api.ts | 79 +++++++++++++++++++++++++ src/shared/schema/mix/actions/editor.ts | 76 +++++++++++++++++++++++- 2 files changed, 153 insertions(+), 2 deletions(-) diff --git a/src/server/controllers/public-api.ts b/src/server/controllers/public-api.ts index d83450a91e..ea061ed8d4 100644 --- a/src/server/controllers/public-api.ts +++ b/src/server/controllers/public-api.ts @@ -21,6 +21,56 @@ const proxyMap: PublicApiRpcMap = { tags: ['navigation'], }, }, + getStructureItems: { + resolve: (api) => api.us.getStructureItems, + openApi: { + summary: 'Get structure list', + tags: ['navigation'], + }, + }, + createWorkbook: { + resolve: (api) => api.us.createWorkbook, + openApi: { + summary: 'Create workbook', + tags: ['navigation'], + }, + }, + createCollection: { + resolve: (api) => api.us.createCollection, + openApi: { + summary: 'Create collection', + tags: ['navigation'], + }, + }, + // connection + getConnection: { + resolve: (api) => api.bi.getConnection, + openApi: { + summary: 'Get connection', + tags: ['connection'], + }, + }, + updateConnection: { + resolve: (api) => api.bi.updateConnection, + openApi: { + summary: 'Update connection', + tags: ['connection'], + }, + }, + createConnection: { + resolve: (api) => api.bi.createConnection, + openApi: { + summary: 'Create connection', + tags: ['connection'], + }, + }, + deleteConnection: { + resolve: (api) => api.bi.deleteConnnection, + openApi: { + summary: 'Delete connection', + tags: ['connection'], + }, + }, // dataset getDataset: { resolve: (api) => api.bi.getDatasetApi, @@ -79,6 +129,35 @@ const proxyMap: PublicApiRpcMap = { tags: ['wizard'], }, }, + // editor + getEditorChart: { + resolve: (api) => api.mix.getEditorChartApi, + openApi: { + summary: 'Get editor chart', + tags: ['editor'], + }, + }, + updateEditorChart: { + resolve: (api) => api.mix.updateEditorChart, + openApi: { + summary: 'Update editor chart', + tags: ['editor'], + }, + }, + createEditorChart: { + resolve: (api) => api.mix.createEditorChart, + openApi: { + summary: 'Create editor chart', + tags: ['editor'], + }, + }, + deleteEditorChart: { + resolve: (api) => api.mix.deleteEditorChartApi, + openApi: { + summary: 'Delete editor chart', + tags: ['editor'], + }, + }, // Dash getDashboard: { resolve: (api) => api.mix.getDashboardApi, diff --git a/src/shared/schema/mix/actions/editor.ts b/src/shared/schema/mix/actions/editor.ts index ac72cfa3e5..deea09add1 100644 --- a/src/shared/schema/mix/actions/editor.ts +++ b/src/shared/schema/mix/actions/editor.ts @@ -1,5 +1,8 @@ -import {DeveloperModeCheckStatus} from '../../../types'; -import {createAction} from '../../gateway-utils'; +import z from 'zod/v4'; + +import {EDITOR_TYPE} from '../../../constants'; +import {DeveloperModeCheckStatus, EntryScope} from '../../../types'; +import {createAction, createTypedAction} from '../../gateway-utils'; import {getTypedApi} from '../../simple-schema'; import type { CreateEditorChartArgs, @@ -10,7 +13,64 @@ import type { import {getEntryLinks} from '../helpers'; import {validateData} from '../helpers/editor/validation'; +const editorUsSchema = z.object({ + data: z.union([ + z.object({ + js: z.string(), + url: z.string(), + params: z.string(), + shared: z.string(), + }), + z.object({ + controls: z.string(), + meta: z.string(), + params: z.string(), + prepare: z.string(), + sources: z.string(), + }), + ]), + entryId: z.string(), + scope: z.literal(EntryScope.Widget), + type: z.enum(EDITOR_TYPE), + public: z.boolean(), + isFavorite: z.boolean(), + createdAt: z.string(), + createdBy: z.string(), + updatedAt: z.string(), + updatedBy: z.string(), + revId: z.string(), + savedId: z.string(), + publishedId: z.string(), + meta: z.record(z.string(), z.string()), + links: z.record(z.string(), z.string()).optional(), + key: z.union([z.null(), z.string()]), + workbookId: z.union([z.null(), z.string()]), +}); + export const editorActions = { + getEditorChartApi: createTypedAction({ + argsSchema: z.object({ + chardId: z.string(), + workbookId: z.union([z.string(), z.null()]).default(null).optional(), + revId: z.string().optional(), + includePermissions: z.boolean().default(false).optional(), + includeLinks: z.boolean().default(false).optional(), + branch: z.literal(['saved', 'published']).default('published').optional(), + }), + bodySchema: editorUsSchema, + }).withValidationSchema(async (api, args) => { + const {includePermissions, includeLinks, revId, chardId, branch, workbookId} = args; + const typedApi = getTypedApi(api); + + return typedApi.us.getEntry({ + entryId: chardId, + includePermissionsInfo: includePermissions ? Boolean(includePermissions) : false, + includeLinks: includeLinks ? Boolean(includeLinks) : false, + ...(revId ? {revId} : {}), + workbookId: workbookId || null, + branch: branch || 'published', + }) as unknown as z.infer; + }), createEditorChart: createAction( async (api, args, {ctx}) => { const {checkRequestForDeveloperModeAccess} = ctx.get('gateway'); @@ -45,4 +105,16 @@ export const editorActions = { } }, ), + deleteEditorChartApi: createTypedAction({ + argsSchema: z.object({ + chartId: z.string(), + }), + bodySchema: z.any(), + }).withValidationSchema(async (api, {chartId}) => { + const typedApi = getTypedApi(api); + + await typedApi.us._deleteUSEntry({ + entryId: chartId, + }); + }), }; From fafce397481f04372ee16251abc73c0137902d24 Mon Sep 17 00:00:00 2001 From: Serge Pavlyuk Date: Fri, 8 Aug 2025 17:22:59 +0300 Subject: [PATCH 09/40] Dash descriptions --- src/shared/sdk/zod-shemas/dash-api.schema.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shared/sdk/zod-shemas/dash-api.schema.ts b/src/shared/sdk/zod-shemas/dash-api.schema.ts index 223600118d..d1393a5275 100644 --- a/src/shared/sdk/zod-shemas/dash-api.schema.ts +++ b/src/shared/sdk/zod-shemas/dash-api.schema.ts @@ -265,6 +265,8 @@ const dataSchema = z.object({ .optional(), tabs: z.array(tabSchema), settings: settingsSchema.optional(), + supportDescription: z.string().optional(), + accessDescription: z.string().optional(), }); // Main dashboard API validation schema From 9e87f79efa5c6b45418e985f0e04e29b2ff236bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladimir=20Stepanenko=20=F0=9F=8F=B4=E2=80=8D=E2=98=A0?= =?UTF-8?q?=EF=B8=8F?= Date: Fri, 29 Aug 2025 15:00:50 +0300 Subject: [PATCH 10/40] Public api features (#2800) --- api/server/components.ts | 9 + api/server/constants.ts | 1 + api/server/controllers.ts | 1 + src/server/components/api-docs/index.ts | 1 + src/server/components/api-docs/types.ts | 14 + src/server/components/app-docs/index.ts | 42 --- .../storage/united-storage/provider.ts | 2 +- .../features/features-list/PublicApi.ts | 10 + .../features-list/PublicApiSwagger.ts | 10 + src/server/components/public-api/constants.ts | 213 +++++++++++++ src/server/components/public-api/index.ts | 7 + .../public-api/types.ts} | 11 +- .../components/public-api/utils/index.ts | 1 + .../utils/init-public-api-swagger.ts | 60 ++++ src/server/constants/public-api.ts | 2 + src/server/controllers/index.ts | 4 +- src/server/controllers/public-api.ts | 297 +++--------------- src/server/expresskit.ts | 3 - src/server/registry/index.ts | 15 + src/server/types/controllers.ts | 3 +- src/server/utils/index.ts | 10 +- src/server/utils/routes.ts | 8 +- src/shared/schema/gateway-utils.ts | 2 +- src/shared/schema/mix/actions/wizard.ts | 2 +- src/shared/types/feature.ts | 4 + 25 files changed, 418 insertions(+), 314 deletions(-) create mode 100644 src/server/components/api-docs/index.ts create mode 100644 src/server/components/api-docs/types.ts delete mode 100644 src/server/components/app-docs/index.ts create mode 100644 src/server/components/features/features-list/PublicApi.ts create mode 100644 src/server/components/features/features-list/PublicApiSwagger.ts create mode 100644 src/server/components/public-api/constants.ts create mode 100644 src/server/components/public-api/index.ts rename src/server/{types/public-api.ts => components/public-api/types.ts} (63%) create mode 100644 src/server/components/public-api/utils/index.ts create mode 100644 src/server/components/public-api/utils/init-public-api-swagger.ts diff --git a/api/server/components.ts b/api/server/components.ts index 33642a9742..f73bd1e43b 100644 --- a/api/server/components.ts +++ b/api/server/components.ts @@ -18,3 +18,12 @@ export { } from '../../src/server/components/charts-engine'; export {renderHTML} from '../../src/server/components/charts-engine/components/markdown'; + +export {initPublicApiSwagger} from '../../src/server/components/public-api'; + +export {PUBLIC_API_PROXY_MAP, PUBLIC_API_ROUTE} from '../../src/server/components/public-api'; +export type { + PublicApiRpcMap, + PublicApiConfig, + PublicApiSecuritySchemes, +} from '../../src/server/components/public-api/types'; diff --git a/api/server/constants.ts b/api/server/constants.ts index cb1832e2ea..bf082fe8a6 100644 --- a/api/server/constants.ts +++ b/api/server/constants.ts @@ -1,2 +1,3 @@ export {SERVICE_NAME_DATALENS} from '../../src/server/constants'; export {IPV6_AXIOS_OPTIONS} from '../../src/server/constants/axios'; +export {PUBLIC_API_ORG_ID_HEADER} from '../../src/server/constants/public-api'; diff --git a/api/server/controllers.ts b/api/server/controllers.ts index a439bbc601..ad2e10e0ac 100644 --- a/api/server/controllers.ts +++ b/api/server/controllers.ts @@ -1,2 +1,3 @@ export {ping} from '../../src/server/controllers/ping'; export {chartsController} from '../../src/server/components/charts-engine/controllers/charts'; +export {createPublicApiController} from '../../src/server/controllers'; diff --git a/src/server/components/api-docs/index.ts b/src/server/components/api-docs/index.ts new file mode 100644 index 0000000000..a50caf907f --- /dev/null +++ b/src/server/components/api-docs/index.ts @@ -0,0 +1 @@ +export type {SecuritySchemeObject} from './types'; diff --git a/src/server/components/api-docs/types.ts b/src/server/components/api-docs/types.ts new file mode 100644 index 0000000000..1fb28b48d7 --- /dev/null +++ b/src/server/components/api-docs/types.ts @@ -0,0 +1,14 @@ +// Copied from @asteasolutions/zod-to-openapi +export type Method = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options' | 'trace'; + +export type SecuritySchemeType = 'apiKey' | 'http' | 'oauth2' | 'openIdConnect'; + +export type SecuritySchemeObject = { + type: SecuritySchemeType; + description?: string; + name?: string; + in?: string; + scheme?: string; + bearerFormat?: string; + openIdConnectUrl?: string; +}; diff --git a/src/server/components/app-docs/index.ts b/src/server/components/app-docs/index.ts deleted file mode 100644 index 43cc8a4305..0000000000 --- a/src/server/components/app-docs/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import {OpenAPIRegistry, OpenApiGeneratorV31} from '@asteasolutions/zod-to-openapi'; -import type {ExpressKit} from '@gravity-ui/expresskit'; -// eslint-disable-next-line import/no-extraneous-dependencies -import swaggerUi from 'swagger-ui-express'; - -export const openApiRegistry = new OpenAPIRegistry(); - -export const initSwagger = ( - app: ExpressKit, - // securitySchemes?: GetAdditionalSecuritySchemesResult, -) => { - const {config} = app; - - const installationText = `Installation – ${config.appInstallation}`; - const envText = `Env – ${config.appEnv}`; - const descriptionText = `
Datalens api.`; - - setImmediate(() => { - openApiRegistry.registerComponent('securitySchemes', 'Access token', { - type: 'apiKey', - in: 'header', - name: 'Authorization', - }); - - app.express.use( - '/api-docs/', - swaggerUi.serve, - swaggerUi.setup( - new OpenApiGeneratorV31(openApiRegistry.definitions).generateDocument({ - openapi: '3.1.0', - info: { - version: `${config.appVersion}`, - title: `UI API `, - description: [installationText, envText, descriptionText].join('
'), - }, - servers: [{url: '/'}], - }), - ), - ); - }); -}; diff --git a/src/server/components/charts-engine/components/storage/united-storage/provider.ts b/src/server/components/charts-engine/components/storage/united-storage/provider.ts index dd34409b2e..071615aa3a 100644 --- a/src/server/components/charts-engine/components/storage/united-storage/provider.ts +++ b/src/server/components/charts-engine/components/storage/united-storage/provider.ts @@ -342,7 +342,7 @@ export class USProvider { }); } - static async retrivePrasedWizardChart( + static async retrieveParsedWizardChart( ctx: AppContext, props: { id: string; diff --git a/src/server/components/features/features-list/PublicApi.ts b/src/server/components/features/features-list/PublicApi.ts new file mode 100644 index 0000000000..7f119b20a0 --- /dev/null +++ b/src/server/components/features/features-list/PublicApi.ts @@ -0,0 +1,10 @@ +import {Feature} from '../../../../shared'; +import {createFeatureConfig} from '../utils'; + +export default createFeatureConfig({ + name: Feature.PublicApi, + state: { + development: false, + production: false, + }, +}); diff --git a/src/server/components/features/features-list/PublicApiSwagger.ts b/src/server/components/features/features-list/PublicApiSwagger.ts new file mode 100644 index 0000000000..8569e03110 --- /dev/null +++ b/src/server/components/features/features-list/PublicApiSwagger.ts @@ -0,0 +1,10 @@ +import {Feature} from '../../../../shared'; +import {createFeatureConfig} from '../utils'; + +export default createFeatureConfig({ + name: Feature.PublicApiSwagger, + state: { + development: false, + production: false, + }, +}); diff --git a/src/server/components/public-api/constants.ts b/src/server/components/public-api/constants.ts new file mode 100644 index 0000000000..bed035757b --- /dev/null +++ b/src/server/components/public-api/constants.ts @@ -0,0 +1,213 @@ +import type {PublicApiRpcMap} from './types'; + +export const PUBLIC_API_HTTP_METHOD = 'POST'; +export const PUBLIC_API_URL = '/rpc/:version/:action'; +export const PUBLIC_API_ROUTE = `${PUBLIC_API_HTTP_METHOD} ${PUBLIC_API_URL}`; + +export const PUBLIC_API_PROXY_MAP = { + v0: { + // navigation + getNavigationList: { + resolve: (api) => api.mix.getNavigationList, + openApi: { + summary: 'Get navigation list', + tags: ['navigation'], + }, + }, + getStructureItems: { + resolve: (api) => api.us.getStructureItems, + openApi: { + summary: 'Get structure list', + tags: ['navigation'], + }, + }, + createWorkbook: { + resolve: (api) => api.us.createWorkbook, + openApi: { + summary: 'Create workbook', + tags: ['navigation'], + }, + }, + createCollection: { + resolve: (api) => api.us.createCollection, + openApi: { + summary: 'Create collection', + tags: ['navigation'], + }, + }, + // connection + getConnection: { + resolve: (api) => api.bi.getConnection, + openApi: { + summary: 'Get connection', + tags: ['connection'], + }, + }, + updateConnection: { + resolve: (api) => api.bi.updateConnection, + openApi: { + summary: 'Update connection', + tags: ['connection'], + }, + }, + createConnection: { + resolve: (api) => api.bi.createConnection, + openApi: { + summary: 'Create connection', + tags: ['connection'], + }, + }, + deleteConnection: { + resolve: (api) => api.bi.deleteConnnection, + openApi: { + summary: 'Delete connection', + tags: ['connection'], + }, + }, + // dataset + getDataset: { + resolve: (api) => api.bi.getDatasetApi, + openApi: { + summary: 'Get dataset', + tags: ['dataset'], + }, + }, + updateDataset: { + resolve: (api) => api.bi.updateDatasetApi, + openApi: { + summary: 'Update dataset', + tags: ['dataset'], + }, + }, + createDataset: { + resolve: (api) => api.bi.createDatasetApi, + openApi: { + summary: 'Create dataset', + tags: ['dataset'], + }, + }, + deleteDataset: { + resolve: (api) => api.bi.deleteDatasetApi, + openApi: { + summary: 'Delete dataset', + tags: ['dataset'], + }, + }, + // wizard + getWizardChart: { + resolve: (api) => api.mix.getWizardChartApi, + openApi: { + summary: 'Get wizard chart', + tags: ['wizard'], + }, + }, + updateWizardChart: { + resolve: (api) => api.mix.updateWizardChartApi, + openApi: { + summary: 'Update wizard chart', + tags: ['wizard'], + }, + }, + createWizardChart: { + resolve: (api) => api.mix.createWizardChartApi, + openApi: { + summary: 'Create wizard chart', + tags: ['wizard'], + }, + }, + deleteWizardChart: { + resolve: (api) => api.mix.deleteWizardChartApi, + openApi: { + summary: 'Delete wizard chart', + tags: ['wizard'], + }, + }, + // editor + getEditorChart: { + resolve: (api) => api.mix.getEditorChartApi, + openApi: { + summary: 'Get editor chart', + tags: ['editor'], + }, + }, + updateEditorChart: { + resolve: (api) => api.mix.updateEditorChart, + openApi: { + summary: 'Update editor chart', + tags: ['editor'], + }, + }, + createEditorChart: { + resolve: (api) => api.mix.createEditorChart, + openApi: { + summary: 'Create editor chart', + tags: ['editor'], + }, + }, + deleteEditorChart: { + resolve: (api) => api.mix.deleteEditorChartApi, + openApi: { + summary: 'Delete editor chart', + tags: ['editor'], + }, + }, + // Dash + getDashboard: { + resolve: (api) => api.mix.getDashboardApi, + openApi: { + summary: 'Get dashboard', + tags: ['dashboard'], + }, + }, + updateDashboard: { + resolve: (api) => api.mix.updateDashboardApi, + openApi: { + summary: 'Delete dashboard', + tags: ['dashboard'], + }, + }, + createDashboard: { + resolve: (api) => api.mix.createDashboardApi, + openApi: { + summary: 'Create dashboard', + tags: ['dashboard'], + }, + }, + deleteDashboard: { + resolve: (api) => api.mix.deleteDashboardApi, + openApi: { + summary: 'Delete dashboard', + tags: ['dashboard'], + }, + }, + // Report + // getReport: { + // resolve: (api) => api.bi.createDataset, + // openApi: { + // summary: 'Get report', + // tags: ['report'], + // }, + // }, + // updateReport: { + // resolve: (api) => api.bi.updateDataset, + // openApi: { + // summary: 'Delete report', + // tags: ['report'], + // }, + // }, + // createReport: { + // resolve: (api) => api.bi.createDataset, + // openApi: { + // summary: 'Create report', + // tags: ['report'], + // }, + // }, + // deleteReport: { + // resolve: (api) => api.bi.deleteDataset, + // openApi: { + // summary: 'Delete report', + // tags: ['report'], + // }, + // }, + }, +} satisfies PublicApiRpcMap; diff --git a/src/server/components/public-api/index.ts b/src/server/components/public-api/index.ts new file mode 100644 index 0000000000..79a123038b --- /dev/null +++ b/src/server/components/public-api/index.ts @@ -0,0 +1,7 @@ +export { + PUBLIC_API_PROXY_MAP, + PUBLIC_API_HTTP_METHOD, + PUBLIC_API_ROUTE, + PUBLIC_API_URL, +} from './constants'; +export {initPublicApiSwagger, publicApiOpenApiRegistry} from './utils'; diff --git a/src/server/types/public-api.ts b/src/server/components/public-api/types.ts similarity index 63% rename from src/server/types/public-api.ts rename to src/server/components/public-api/types.ts index a654cd2e78..529334bcee 100644 --- a/src/server/types/public-api.ts +++ b/src/server/components/public-api/types.ts @@ -1,7 +1,8 @@ import type {Request, Response} from '@gravity-ui/expresskit'; import type {ApiWithRoot, SchemasByScope} from '@gravity-ui/gateway'; -import type {DatalensGatewaySchemas} from './gateway'; +import type {DatalensGatewaySchemas} from '../../types/gateway'; +import type {SecuritySchemeObject} from '../api-docs'; type HeadersType = Record; @@ -20,3 +21,11 @@ export type PublicApiRpcMap >; + +export type PublicApiSecuritySchemes = Record; + +export type PublicApiConfig = { + proxyMap: PublicApiRpcMap; + securitySchemes: PublicApiSecuritySchemes; + securityTypes: string[]; +}; diff --git a/src/server/components/public-api/utils/index.ts b/src/server/components/public-api/utils/index.ts new file mode 100644 index 0000000000..ad6e980fb4 --- /dev/null +++ b/src/server/components/public-api/utils/index.ts @@ -0,0 +1 @@ +export {initPublicApiSwagger, publicApiOpenApiRegistry} from './init-public-api-swagger'; diff --git a/src/server/components/public-api/utils/init-public-api-swagger.ts b/src/server/components/public-api/utils/init-public-api-swagger.ts new file mode 100644 index 0000000000..19d1d81408 --- /dev/null +++ b/src/server/components/public-api/utils/init-public-api-swagger.ts @@ -0,0 +1,60 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import {OpenAPIRegistry, OpenApiGeneratorV31} from '@asteasolutions/zod-to-openapi'; +// eslint-disable-next-line import/no-extraneous-dependencies +import type {OpenAPIObjectConfigV31} from '@asteasolutions/zod-to-openapi/dist/v3.1/openapi-generator'; +import type {ExpressKit} from '@gravity-ui/expresskit'; +// eslint-disable-next-line import/no-extraneous-dependencies +import swaggerUi from 'swagger-ui-express'; + +import type {PublicApiSecuritySchemes} from '../types'; + +export const publicApiOpenApiRegistry = new OpenAPIRegistry(); + +export const initPublicApiSwagger = ( + app: ExpressKit, + securitySchemes?: PublicApiSecuritySchemes, +) => { + const {config} = app; + + const installationText = `Installation – ${config.appInstallation}`; + const envText = `Env – ${config.appEnv}`; + const descriptionText = `
Datalens api.`; + + setImmediate(() => { + if (securitySchemes) { + Object.keys(securitySchemes).forEach((securityType) => { + publicApiOpenApiRegistry.registerComponent('securitySchemes', securityType, { + ...securitySchemes[securityType], + }); + }); + } + + const generator = new OpenApiGeneratorV31(publicApiOpenApiRegistry.definitions); + + const generateDocumentParams: OpenAPIObjectConfigV31 = { + openapi: '3.1.0', + info: { + version: `${config.appVersion}`, + title: `UI API `, + description: [installationText, envText, descriptionText].join('
'), + }, + servers: [{url: '/'}], + }; + + const openApiDocument = generator.generateDocument(generateDocumentParams); + + app.express.get('/api-docs.json', (req, res) => { + const host = req.get('host'); + const serverUrl = `https://${host}`; + + const result: typeof openApiDocument = { + ...openApiDocument, + servers: [{url: serverUrl}], + }; + + return res.json(result); + }); + + app.express.use('/api-docs/', swaggerUi.serve, swaggerUi.setup(openApiDocument)); + }); +}; diff --git a/src/server/constants/public-api.ts b/src/server/constants/public-api.ts index e38f35de71..6c1bbfb0a3 100644 --- a/src/server/constants/public-api.ts +++ b/src/server/constants/public-api.ts @@ -1 +1,3 @@ export const PUBLIC_API_RPC_ERROR_CODE = 'ERR.UI_API.PUBLIC-API.FAILED_RPC_PROXY'; + +export const PUBLIC_API_ORG_ID_HEADER = 'x-dl-org-id'; diff --git a/src/server/controllers/index.ts b/src/server/controllers/index.ts index 24cfbbc4cd..bc303a6fa9 100644 --- a/src/server/controllers/index.ts +++ b/src/server/controllers/index.ts @@ -2,12 +2,12 @@ import {apiControllers} from './api'; import {dlMainController} from './dl-main'; import {navigateController} from './navigate'; import {navigationController} from './navigation'; -import {publicApiControllerGetter} from './public-api'; +import {createPublicApiController} from './public-api'; export { apiControllers, dlMainController, navigateController, navigationController, - publicApiControllerGetter, + createPublicApiController, }; diff --git a/src/server/controllers/public-api.ts b/src/server/controllers/public-api.ts index ea061ed8d4..d4dc22c4aa 100644 --- a/src/server/controllers/public-api.ts +++ b/src/server/controllers/public-api.ts @@ -1,224 +1,22 @@ import type {Request, Response} from '@gravity-ui/expresskit'; +import type {AppContext} from '@gravity-ui/nodekit'; import {REQUEST_ID_PARAM_NAME} from '@gravity-ui/nodekit'; import _ from 'lodash'; import z from 'zod/v4'; +import {Feature, isEnabledServerFeature} from '../../shared'; import {getValidationSchema, hasValidationSchema} from '../../shared/schema/gateway-utils'; -import {openApiRegistry} from '../components/app-docs'; +import { + PUBLIC_API_HTTP_METHOD, + PUBLIC_API_URL, + publicApiOpenApiRegistry, +} from '../components/public-api'; +import type {PublicApiRpcMap} from '../components/public-api/types'; import {PUBLIC_API_RPC_ERROR_CODE} from '../constants/public-api'; import {registry} from '../registry'; import type {DatalensGatewaySchemas} from '../types/gateway'; -import type {PublicApiRpcMap} from '../types/public-api'; import Utils from '../utils'; -const proxyMap: PublicApiRpcMap = { - v0: { - // navigation - getNavigationList: { - resolve: (api) => api.mix.getNavigationList, - openApi: { - summary: 'Get navigation list', - tags: ['navigation'], - }, - }, - getStructureItems: { - resolve: (api) => api.us.getStructureItems, - openApi: { - summary: 'Get structure list', - tags: ['navigation'], - }, - }, - createWorkbook: { - resolve: (api) => api.us.createWorkbook, - openApi: { - summary: 'Create workbook', - tags: ['navigation'], - }, - }, - createCollection: { - resolve: (api) => api.us.createCollection, - openApi: { - summary: 'Create collection', - tags: ['navigation'], - }, - }, - // connection - getConnection: { - resolve: (api) => api.bi.getConnection, - openApi: { - summary: 'Get connection', - tags: ['connection'], - }, - }, - updateConnection: { - resolve: (api) => api.bi.updateConnection, - openApi: { - summary: 'Update connection', - tags: ['connection'], - }, - }, - createConnection: { - resolve: (api) => api.bi.createConnection, - openApi: { - summary: 'Create connection', - tags: ['connection'], - }, - }, - deleteConnection: { - resolve: (api) => api.bi.deleteConnnection, - openApi: { - summary: 'Delete connection', - tags: ['connection'], - }, - }, - // dataset - getDataset: { - resolve: (api) => api.bi.getDatasetApi, - openApi: { - summary: 'Get dataset', - tags: ['dataset'], - }, - }, - updateDataset: { - resolve: (api) => api.bi.updateDatasetApi, - openApi: { - summary: 'Update dataset', - tags: ['dataset'], - }, - }, - createDataset: { - resolve: (api) => api.bi.createDatasetApi, - openApi: { - summary: 'Create dataset', - tags: ['dataset'], - }, - }, - deleteDataset: { - resolve: (api) => api.bi.deleteDatasetApi, - openApi: { - summary: 'Delete dataset', - tags: ['dataset'], - }, - }, - // wizard - getWizardChart: { - resolve: (api) => api.mix.getWizardChartApi, - openApi: { - summary: 'Get wizard chart', - tags: ['wizard'], - }, - }, - updateWizardChart: { - resolve: (api) => api.mix.updateWizardChartApi, - openApi: { - summary: 'Update wizard chart', - tags: ['wizard'], - }, - }, - createWizardChart: { - resolve: (api) => api.mix.createWizardChartApi, - openApi: { - summary: 'Create wizard chart', - tags: ['wizard'], - }, - }, - deleteWizardChart: { - resolve: (api) => api.mix.deleteWizardChartApi, - openApi: { - summary: 'Delete wizard chart', - tags: ['wizard'], - }, - }, - // editor - getEditorChart: { - resolve: (api) => api.mix.getEditorChartApi, - openApi: { - summary: 'Get editor chart', - tags: ['editor'], - }, - }, - updateEditorChart: { - resolve: (api) => api.mix.updateEditorChart, - openApi: { - summary: 'Update editor chart', - tags: ['editor'], - }, - }, - createEditorChart: { - resolve: (api) => api.mix.createEditorChart, - openApi: { - summary: 'Create editor chart', - tags: ['editor'], - }, - }, - deleteEditorChart: { - resolve: (api) => api.mix.deleteEditorChartApi, - openApi: { - summary: 'Delete editor chart', - tags: ['editor'], - }, - }, - // Dash - getDashboard: { - resolve: (api) => api.mix.getDashboardApi, - openApi: { - summary: 'Get dashboard', - tags: ['dashboard'], - }, - }, - updateDashboard: { - resolve: (api) => api.mix.updateDashboardApi, - openApi: { - summary: 'Delete dashboard', - tags: ['dashboard'], - }, - }, - createDashboard: { - resolve: (api) => api.mix.createDashboardApi, - openApi: { - summary: 'Create dashboard', - tags: ['dashboard'], - }, - }, - deleteDashboard: { - resolve: (api) => api.mix.deleteDashboardApi, - openApi: { - summary: 'Delete dashboard', - tags: ['dashboard'], - }, - }, - // Report - // getReport: { - // resolve: (api) => api.bi.createDataset, - // openApi: { - // summary: 'Get report', - // tags: ['report'], - // }, - // }, - // updateReport: { - // resolve: (api) => api.bi.updateDataset, - // openApi: { - // summary: 'Delete report', - // tags: ['report'], - // }, - // }, - // createReport: { - // resolve: (api) => api.bi.createDataset, - // openApi: { - // summary: 'Create report', - // tags: ['report'], - // }, - // }, - // deleteReport: { - // resolve: (api) => api.bi.deleteDataset, - // openApi: { - // summary: 'Delete report', - // tags: ['report'], - // }, - // }, - }, -}; - const handleError = (req: Request, res: Response, status: number, message: string) => { res.status(status).send({ status, @@ -228,18 +26,8 @@ const handleError = (req: Request, res: Response, status: number, message: strin }); }; -const parseRoute = (route: string) => { - const spacerIndex = route.indexOf(' '); - const method = route.slice(0, spacerIndex).trim(); - const url = route.slice(spacerIndex).trim(); - - return { - method, - url, - reverse: (props: {version: string; action: string}) => { - return url.replace(':version', props.version).replace(':action', props.action); - }, - }; +const resolveUrl = ({version, action}: {version: string; action: string}) => { + return PUBLIC_API_URL.replace(':version', version).replace(':action', action); }; const defaultSchema = { @@ -265,36 +53,39 @@ const defaultSchema = { }, }; -export function publicApiControllerGetter( - gatewayProxyMap: PublicApiRpcMap = proxyMap, - params: any, -) { - const parsedRoute = parseRoute(params.route); +export function createPublicApiController(ctx: AppContext) { const {gatewayApi} = registry.getGatewayApi(); - - Object.entries(gatewayProxyMap).forEach(([version, actions]) => { - Object.entries(actions).forEach(([action, {resolve, openApi}]) => { - const gatewayApiAction = resolve(gatewayApi); - - if (hasValidationSchema(gatewayApiAction)) { - openApiRegistry.registerPath({ - method: parsedRoute.method.toLocaleLowerCase(), - path: parsedRoute.reverse({version, action}), - ...openApi, - ...getValidationSchema(gatewayApiAction)().getOpenApichema(), - security: [{['Access token']: []}], - }); - } else { - openApiRegistry.registerPath({ - method: parsedRoute.method.toLocaleLowerCase(), - path: parsedRoute.reverse({version, action}), - ...openApi, - ...defaultSchema, - security: [{['Access token']: []}], - } as any); - } + const {proxyMap, securityTypes} = registry.getPublicApiConfig(); + + if (isEnabledServerFeature(ctx, Feature.PublicApiSwagger)) { + const security = securityTypes.map((type) => ({ + [type]: [], + })); + + Object.entries(proxyMap).forEach(([version, actions]) => { + Object.entries(actions).forEach(([action, {resolve, openApi}]) => { + const gatewayApiAction = resolve(gatewayApi); + + if (hasValidationSchema(gatewayApiAction)) { + publicApiOpenApiRegistry.registerPath({ + method: PUBLIC_API_HTTP_METHOD.toLocaleLowerCase(), + path: resolveUrl({version, action}), + ...openApi, + ...getValidationSchema(gatewayApiAction)().getOpenApiSchema(), + security, + }); + } else { + publicApiOpenApiRegistry.registerPath({ + method: PUBLIC_API_HTTP_METHOD.toLocaleLowerCase(), + path: resolveUrl({version, action}), + ...openApi, + ...defaultSchema, + security, + } as any); + } + }); }); - }); + } return async function publicApiController(req: Request, res: Response) { const boundeHandler = handleError.bind(null, req, res); @@ -304,13 +95,13 @@ export function publicApiControllerGetter( } const version = req.params.version as keyof PublicApiRpcMap; - if (!_.has(gatewayProxyMap, version)) { + if (!_.has(proxyMap, version)) { return boundeHandler(404, 'Version not found'); } - const versionMap = gatewayProxyMap[version]; + const versionMap = proxyMap[version]; const actionName = req.params.action as keyof typeof versionMap; - if (!_.has(gatewayProxyMap[version], req.params.action)) { + if (!_.has(proxyMap[version], req.params.action)) { return boundeHandler(404, 'Action not found'); } diff --git a/src/server/expresskit.ts b/src/server/expresskit.ts index 6e9cb86b56..66e12cc043 100644 --- a/src/server/expresskit.ts +++ b/src/server/expresskit.ts @@ -2,7 +2,6 @@ import type {AppRoutes} from '@gravity-ui/expresskit'; import {ExpressKit} from '@gravity-ui/expresskit'; import type {NodeKit} from '@gravity-ui/nodekit'; -import {initSwagger} from './components/app-docs'; import type {ExtendedAppRouteDescription} from './types/controllers'; export function getExpressKit({ @@ -23,7 +22,5 @@ export function getExpressKit({ const app = new ExpressKit(nodekit, routes); - initSwagger(app); - return app; } diff --git a/src/server/registry/index.ts b/src/server/registry/index.ts index b9aace5aae..c97f1e1e4e 100644 --- a/src/server/registry/index.ts +++ b/src/server/registry/index.ts @@ -7,6 +7,7 @@ import _ from 'lodash'; import {getValidationSchema, registerValidationSchema} from '../../shared/schema/gateway-utils'; import type {ChartsEngine} from '../components/charts-engine'; +import type {PublicApiConfig} from '../components/public-api/types'; import type {QLConnectionTypeMap} from '../modes/charts/plugins/ql/utils/connection'; import {getConnectorToQlConnectionTypeMap} from '../modes/charts/plugins/ql/utils/connection'; import type {GetLayoutConfig} from '../types/app-layout'; @@ -63,6 +64,7 @@ let getLayoutConfig: GetLayoutConfig | undefined; let yfmPlugins: MarkdownItPluginCb[]; let getXlsxConverter: XlsxConverterFn | undefined; let qLConnectionTypeMap: QLConnectionTypeMap | undefined; +let publicApiConfig: PublicApiConfig | undefined; export const registry = { common: commonRegistry, @@ -164,4 +166,17 @@ export const registry = { getQLConnectionTypeMap() { return qLConnectionTypeMap ?? getConnectorToQlConnectionTypeMap(); }, + setupPublicApiConfig(config: PublicApiConfig) { + if (publicApiConfig) { + throw new Error('The method must not be called more than once [setupPublicApiConfig]'); + } + publicApiConfig = config; + }, + getPublicApiConfig() { + if (!publicApiConfig) { + throw new Error('First of all setup the publicApiConfig'); + } + + return publicApiConfig; + }, }; diff --git a/src/server/types/controllers.ts b/src/server/types/controllers.ts index fb706d8c22..4bd6a22479 100644 --- a/src/server/types/controllers.ts +++ b/src/server/types/controllers.ts @@ -21,5 +21,4 @@ export type BasicControllers = | 'navigate' | 'navigation' | 'api.deleteLock' - | 'schematic-gateway' - | 'public-api'; + | 'schematic-gateway'; diff --git a/src/server/utils/index.ts b/src/server/utils/index.ts index 1bc5d70ac4..65725b99ff 100644 --- a/src/server/utils/index.ts +++ b/src/server/utils/index.ts @@ -17,8 +17,10 @@ import { SuperuserHeader, TENANT_ID_HEADER, US_MASTER_TOKEN_HEADER, + makeTenantIdFromOrgId, } from '../../shared'; import {isOpensourceInstallation} from '../app-env'; +import {PUBLIC_API_ORG_ID_HEADER} from '../constants/public-api'; import {isGatewayError} from './gateway'; @@ -85,9 +87,15 @@ class Utils { } static pickRpcHeaders(req: Request) { + const headersMap = req.ctx.config.headersMap; + + const orgId = req.headers[PUBLIC_API_ORG_ID_HEADER]; + const tenantId = orgId && !Array.isArray(orgId) ? makeTenantIdFromOrgId(orgId) : undefined; + return { - ...pick(req.headers, [AuthHeader.Authorization]), + ...pick(req.headers, [AuthHeader.Authorization, headersMap.subjectToken]), ...Utils.pickForwardHeaders(req.headers), + [TENANT_ID_HEADER]: tenantId, }; } diff --git a/src/server/utils/routes.ts b/src/server/utils/routes.ts index 80eea2faeb..da59b01d8e 100644 --- a/src/server/utils/routes.ts +++ b/src/server/utils/routes.ts @@ -3,7 +3,6 @@ import { dlMainController, navigateController, navigationController, - publicApiControllerGetter, } from '../controllers'; import {registry} from '../registry'; import type {BasicControllers, ExtendedAppRouteDescription} from '../types/controllers'; @@ -41,12 +40,7 @@ export const getConfiguredRoute = ( ...params, }; } - case 'public-api': { - return { - handler: publicApiControllerGetter(undefined, params), - ...params, - }; - } + default: return null as never; } diff --git a/src/shared/schema/gateway-utils.ts b/src/shared/schema/gateway-utils.ts index 361b5140ca..57c323ec06 100644 --- a/src/shared/schema/gateway-utils.ts +++ b/src/shared/schema/gateway-utils.ts @@ -79,7 +79,7 @@ export function createTypedAction< getSchema() { return schema; }, - getOpenApichema() { + getOpenApiSchema() { return { request: { body: { diff --git a/src/shared/schema/mix/actions/wizard.ts b/src/shared/schema/mix/actions/wizard.ts index 9ff736b352..53b80ddeb0 100644 --- a/src/shared/schema/mix/actions/wizard.ts +++ b/src/shared/schema/mix/actions/wizard.ts @@ -39,7 +39,7 @@ export const wizardActions = { }).withValidationSchema(async (_, args, {ctx, headers}) => { const {includePermissions, includeLinks, unreleased, revId, chardId} = args; - const result = await USProvider.retrivePrasedWizardChart(ctx, { + const result = await USProvider.retrieveParsedWizardChart(ctx, { id: chardId, includePermissionsInfo: includePermissions ? includePermissions?.toString() : '0', includeLinks: includeLinks ? includeLinks?.toString() : '0', diff --git a/src/shared/types/feature.ts b/src/shared/types/feature.ts index 137550e787..88d6142e3a 100644 --- a/src/shared/types/feature.ts +++ b/src/shared/types/feature.ts @@ -100,6 +100,10 @@ export enum Feature { GravityChartsForPieAndTreemap = 'GravityChartsForPieAndTreemap', /** Use GravityUI Charts as the default library for some wizard visualizations(scatter and bar-y) */ GravityChartsForBarYAndScatter = 'GravityChartsForBarYAndScatter', + /** Enable public api route in api AppMode */ + PublicApi = 'PublicApi', + /** Enable public api swagger */ + PublicApiSwagger = 'PublicApiSwagger', } export type FeatureConfig = Record; From 8fcbfd292eae005311dc647f3904dee4d19ed151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladimir=20Stepanenko=20=F0=9F=8F=B4=E2=80=8D=E2=98=A0?= =?UTF-8?q?=EF=B8=8F?= Date: Sun, 7 Sep 2025 23:57:08 +0300 Subject: [PATCH 11/40] Split api and public api (#2839) --- api/server/app-env.ts | 1 + src/server/app-env.ts | 2 + .../features/features-list/PublicApi.ts | 10 --- .../features-list/PublicApiSwagger.ts | 10 --- .../utils/init-public-api-swagger.ts | 82 +++++++++++++++++++ src/server/controllers/public-api.ts | 70 +--------------- src/server/modes/opensource/app.ts | 2 +- src/server/registry/index.ts | 47 +++-------- src/shared/constants/common.ts | 1 + src/shared/types/feature.ts | 4 - 10 files changed, 99 insertions(+), 130 deletions(-) delete mode 100644 src/server/components/features/features-list/PublicApi.ts delete mode 100644 src/server/components/features/features-list/PublicApiSwagger.ts diff --git a/api/server/app-env.ts b/api/server/app-env.ts index 05f8e1cac8..93826c6aa6 100644 --- a/api/server/app-env.ts +++ b/api/server/app-env.ts @@ -4,4 +4,5 @@ export { isDatalensMode, isFullMode, isApiMode, + isPublicApiMode, } from '../../src/server/app-env'; diff --git a/src/server/app-env.ts b/src/server/app-env.ts index 741cf23d52..bd7cd29492 100644 --- a/src/server/app-env.ts +++ b/src/server/app-env.ts @@ -10,4 +10,6 @@ export const isFullMode = mode === AppMode.Full; export const isDatalensMode = mode === AppMode.Datalens; export const isChartsMode = mode === AppMode.Charts; export const isApiMode = mode === AppMode.Api; +export const isPublicApiMode = mode === AppMode.PublicApi; + export const isOpensourceInstallation = appInstallation === AppInstallation.Opensource; diff --git a/src/server/components/features/features-list/PublicApi.ts b/src/server/components/features/features-list/PublicApi.ts deleted file mode 100644 index 7f119b20a0..0000000000 --- a/src/server/components/features/features-list/PublicApi.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {Feature} from '../../../../shared'; -import {createFeatureConfig} from '../utils'; - -export default createFeatureConfig({ - name: Feature.PublicApi, - state: { - development: false, - production: false, - }, -}); diff --git a/src/server/components/features/features-list/PublicApiSwagger.ts b/src/server/components/features/features-list/PublicApiSwagger.ts deleted file mode 100644 index 8569e03110..0000000000 --- a/src/server/components/features/features-list/PublicApiSwagger.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {Feature} from '../../../../shared'; -import {createFeatureConfig} from '../utils'; - -export default createFeatureConfig({ - name: Feature.PublicApiSwagger, - state: { - development: false, - production: false, - }, -}); diff --git a/src/server/components/public-api/utils/init-public-api-swagger.ts b/src/server/components/public-api/utils/init-public-api-swagger.ts index 19d1d81408..fd6eb8fc85 100644 --- a/src/server/components/public-api/utils/init-public-api-swagger.ts +++ b/src/server/components/public-api/utils/init-public-api-swagger.ts @@ -3,13 +3,46 @@ import {OpenAPIRegistry, OpenApiGeneratorV31} from '@asteasolutions/zod-to-opena // eslint-disable-next-line import/no-extraneous-dependencies import type {OpenAPIObjectConfigV31} from '@asteasolutions/zod-to-openapi/dist/v3.1/openapi-generator'; import type {ExpressKit} from '@gravity-ui/expresskit'; +import {AppError} from '@gravity-ui/nodekit'; // eslint-disable-next-line import/no-extraneous-dependencies import swaggerUi from 'swagger-ui-express'; +import z from 'zod/v4'; +import {getValidationSchema, hasValidationSchema} from '../../../../shared/schema/gateway-utils'; +import {registry} from '../../../registry'; +import type {DatalensGatewaySchemas} from '../../../types/gateway'; +import {PUBLIC_API_HTTP_METHOD, PUBLIC_API_URL} from '../constants'; import type {PublicApiSecuritySchemes} from '../types'; export const publicApiOpenApiRegistry = new OpenAPIRegistry(); +const resolveUrl = ({version, action}: {version: string; action: string}) => { + return PUBLIC_API_URL.replace(':version', version).replace(':action', action); +}; + +const defaultSchema = { + summary: 'Type not defined', + request: { + body: { + content: { + ['application/json']: { + schema: z.toJSONSchema(z.any()), + }, + }, + }, + }, + responses: { + 200: { + description: 'TBD', + content: { + ['application/json']: { + schema: z.toJSONSchema(z.any()), + }, + }, + }, + }, +}; + export const initPublicApiSwagger = ( app: ExpressKit, securitySchemes?: PublicApiSecuritySchemes, @@ -21,6 +54,55 @@ export const initPublicApiSwagger = ( const descriptionText = `
Datalens api.`; setImmediate(() => { + const {gatewayApi} = registry.getGatewayApi(); + const schemasByScope = registry.getGatewaySchemasByScope(); + const {proxyMap, securityTypes} = registry.getPublicApiConfig(); + + const actionToPathMap = new Map(); + + Object.entries(gatewayApi).forEach(([serviceName, actions]) => { + Object.entries(actions).forEach(([actionName, action]) => { + actionToPathMap.set(action, {serviceName, actionName}); + }); + }); + + const security = securityTypes.map((type) => ({ + [type]: [], + })); + + Object.entries(proxyMap).forEach(([version, actions]) => { + Object.entries(actions).forEach(([action, {resolve, openApi}]) => { + const gatewayApiAction = resolve(gatewayApi); + + const pathObject = actionToPathMap.get(gatewayApiAction); + + if (!pathObject) { + throw new AppError('Public api proxyMap action not found in gatewayApi.'); + } + + const actionConfig = + schemasByScope.root[pathObject.serviceName].actions[pathObject.actionName]; + + if (hasValidationSchema(actionConfig)) { + publicApiOpenApiRegistry.registerPath({ + method: PUBLIC_API_HTTP_METHOD.toLocaleLowerCase(), + path: resolveUrl({version, action}), + ...openApi, + ...getValidationSchema(actionConfig)().getOpenApiSchema(), + security, + }); + } else { + publicApiOpenApiRegistry.registerPath({ + method: PUBLIC_API_HTTP_METHOD.toLocaleLowerCase(), + path: resolveUrl({version, action}), + ...openApi, + ...defaultSchema, + security, + } as any); + } + }); + }); + if (securitySchemes) { Object.keys(securitySchemes).forEach((securityType) => { publicApiOpenApiRegistry.registerComponent('securitySchemes', securityType, { diff --git a/src/server/controllers/public-api.ts b/src/server/controllers/public-api.ts index d4dc22c4aa..eb7f499234 100644 --- a/src/server/controllers/public-api.ts +++ b/src/server/controllers/public-api.ts @@ -1,16 +1,7 @@ import type {Request, Response} from '@gravity-ui/expresskit'; -import type {AppContext} from '@gravity-ui/nodekit'; import {REQUEST_ID_PARAM_NAME} from '@gravity-ui/nodekit'; import _ from 'lodash'; -import z from 'zod/v4'; -import {Feature, isEnabledServerFeature} from '../../shared'; -import {getValidationSchema, hasValidationSchema} from '../../shared/schema/gateway-utils'; -import { - PUBLIC_API_HTTP_METHOD, - PUBLIC_API_URL, - publicApiOpenApiRegistry, -} from '../components/public-api'; import type {PublicApiRpcMap} from '../components/public-api/types'; import {PUBLIC_API_RPC_ERROR_CODE} from '../constants/public-api'; import {registry} from '../registry'; @@ -26,66 +17,9 @@ const handleError = (req: Request, res: Response, status: number, message: strin }); }; -const resolveUrl = ({version, action}: {version: string; action: string}) => { - return PUBLIC_API_URL.replace(':version', version).replace(':action', action); -}; - -const defaultSchema = { - summary: 'Type not defined', - request: { - body: { - content: { - ['application/json']: { - schema: z.toJSONSchema(z.any()), - }, - }, - }, - }, - responses: { - 200: { - description: 'TBD', - content: { - ['application/json']: { - schema: z.toJSONSchema(z.any()), - }, - }, - }, - }, -}; - -export function createPublicApiController(ctx: AppContext) { +export function createPublicApiController() { const {gatewayApi} = registry.getGatewayApi(); - const {proxyMap, securityTypes} = registry.getPublicApiConfig(); - - if (isEnabledServerFeature(ctx, Feature.PublicApiSwagger)) { - const security = securityTypes.map((type) => ({ - [type]: [], - })); - - Object.entries(proxyMap).forEach(([version, actions]) => { - Object.entries(actions).forEach(([action, {resolve, openApi}]) => { - const gatewayApiAction = resolve(gatewayApi); - - if (hasValidationSchema(gatewayApiAction)) { - publicApiOpenApiRegistry.registerPath({ - method: PUBLIC_API_HTTP_METHOD.toLocaleLowerCase(), - path: resolveUrl({version, action}), - ...openApi, - ...getValidationSchema(gatewayApiAction)().getOpenApiSchema(), - security, - }); - } else { - publicApiOpenApiRegistry.registerPath({ - method: PUBLIC_API_HTTP_METHOD.toLocaleLowerCase(), - path: resolveUrl({version, action}), - ...openApi, - ...defaultSchema, - security, - } as any); - } - }); - }); - } + const {proxyMap} = registry.getPublicApiConfig(); return async function publicApiController(req: Request, res: Response) { const boundeHandler = handleError.bind(null, req, res); diff --git a/src/server/modes/opensource/app.ts b/src/server/modes/opensource/app.ts index b4c5ff63fc..e5f5ccc479 100644 --- a/src/server/modes/opensource/app.ts +++ b/src/server/modes/opensource/app.ts @@ -139,7 +139,7 @@ function initApiApp({ beforeAuth: AppMiddleware[]; afterAuth: AppMiddleware[]; }) { - // As charts app execpt chartEngine + // As charts app except chartEngine if (isApiMode) { afterAuth.push(xDlContext(), setSubrequestHeaders, patchLogger, getCtxMiddleware()); beforeAuth.push(beforeAuthDefaults); diff --git a/src/server/registry/index.ts b/src/server/registry/index.ts index c97f1e1e4e..0babce8eed 100644 --- a/src/server/registry/index.ts +++ b/src/server/registry/index.ts @@ -5,7 +5,6 @@ import {getGatewayControllers} from '@gravity-ui/gateway'; import type {AppContext} from '@gravity-ui/nodekit'; import _ from 'lodash'; -import {getValidationSchema, registerValidationSchema} from '../../shared/schema/gateway-utils'; import type {ChartsEngine} from '../components/charts-engine'; import type {PublicApiConfig} from '../components/public-api/types'; import type {QLConnectionTypeMap} from '../modes/charts/plugins/ql/utils/connection'; @@ -21,44 +20,10 @@ let chartsEngine: ChartsEngine; export const wrapperGetGatewayControllers = ( schemasByScope: SchemasByScope, config: GatewayConfig, -) => { - const typedSchemasMap = Object.keys(schemasByScope).reduce>( - (memo, scope) => { - const services = schemasByScope[scope]; - Object.keys(services).forEach((service) => { - const actions = services[service].actions; - - Object.entries(actions).forEach(([action, actionConfig]) => { - const validationSchema = getValidationSchema(actionConfig); - - if (validationSchema) { - memo[`${scope}.${service}.${action}`] = validationSchema; - } - }); - }); - - return memo; - }, - {}, - ); - - const controllers = getGatewayControllers( - schemasByScope, - config, - ); - - Object.entries(typedSchemasMap).forEach(([actionPath, schema]) => { - const actionCallback = _.get(controllers.api, actionPath, null); - - if (actionCallback) { - registerValidationSchema(actionCallback, schema); - } - }); - - return controllers; -}; +) => getGatewayControllers(schemasByScope, config); let gateway: ReturnType; +let gatewaySchemasByScope: SchemasByScope; let publicSchema: any; let getLayoutConfig: GetLayoutConfig | undefined; let yfmPlugins: MarkdownItPluginCb[]; @@ -102,6 +67,7 @@ export const registry = { } gateway = wrapperGetGatewayControllers(schemasByScope, config); publicSchema = publicSchemaArg; + gatewaySchemasByScope = schemasByScope; }, getGatewayController() { if (!gateway) { @@ -119,6 +85,13 @@ export const registry = { gatewayApi: ApiWithRoot; }; }, + getGatewaySchemasByScope() { + if (!gatewaySchemasByScope) { + throw new Error('First of all setup the gateway'); + } + + return gatewaySchemasByScope; + }, getPublicApi() { if (!publicSchema) { throw new Error('First of all setup the publicSchema'); diff --git a/src/shared/constants/common.ts b/src/shared/constants/common.ts index 62aa493452..5309013f30 100644 --- a/src/shared/constants/common.ts +++ b/src/shared/constants/common.ts @@ -19,6 +19,7 @@ export enum AppMode { Datalens = 'datalens', Charts = 'charts', Api = 'api', + PublicApi = 'public-api', } export enum Language { diff --git a/src/shared/types/feature.ts b/src/shared/types/feature.ts index 589f590d5a..1a54d5b890 100644 --- a/src/shared/types/feature.ts +++ b/src/shared/types/feature.ts @@ -96,10 +96,6 @@ export enum Feature { GravityChartsForPieAndTreemap = 'GravityChartsForPieAndTreemap', /** Use GravityUI Charts as the default library for some wizard visualizations(scatter and bar-y) */ GravityChartsForBarYAndScatter = 'GravityChartsForBarYAndScatter', - /** Enable public api route in api AppMode */ - PublicApi = 'PublicApi', - /** Enable public api swagger */ - PublicApiSwagger = 'PublicApiSwagger', /** Save field settings (formatting and colors) in the dataset */ StoreFieldSettingsAtDataset = 'StoreFieldSettingsAtDataset', /** Enable dataset revisions */ From b88c0164e857693e9670235f77f12edb6de1d270 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Mon, 8 Sep 2025 10:06:55 +0300 Subject: [PATCH 12/40] Add packages --- package-lock.json | 88 ++++++++++++++++--- package.json | 3 + .../utils/init-public-api-swagger.ts | 3 - src/server/registry/index.ts | 10 --- 4 files changed, 81 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index ee7e4d71a6..3096a01ebe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "license": "Apache-2.0", "dependencies": { + "@asteasolutions/zod-to-openapi": "^7.3.4", "@braintree/sanitize-url": "^6.0.0", "@datalens-tech/ui-sandbox-modules": "^0.36.0", "@datalens-tech/xlsx": "^0.20.1", @@ -73,6 +74,7 @@ "request-ip": "^3.3.0", "request-promise-native": "^1.0.9", "set-cookie-parser": "^2.7.1", + "swagger-ui-express": "^5.0.1", "workerpool": "^9.1.1", "zod": "^3.25.64" }, @@ -137,6 +139,7 @@ "@types/request-ip": "^0.0.41", "@types/request-promise-native": "^1.0.21", "@types/set-cookie-parser": "^2.4.10", + "@types/swagger-ui-express": "^4.1.8", "@types/uuid": "^9.0.8", "@types/webpack-env": "^1.16.0", "bem-cn-lite": "^4.0.0", @@ -236,6 +239,18 @@ "node": ">=6.0.0" } }, + "node_modules/@asteasolutions/zod-to-openapi": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-7.3.4.tgz", + "integrity": "sha512-/2rThQ5zPi9OzVwes6U7lK1+Yvug0iXu25olp7S0XsYmOqnyMfxH7gdSQjn/+DSOHRg7wnotwGJSyL+fBKdnEA==", + "license": "MIT", + "dependencies": { + "openapi3-ts": "^4.1.2" + }, + "peerDependencies": { + "zod": "^3.20.2" + } + }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -9335,6 +9350,13 @@ } } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -12606,6 +12628,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/@types/tapable": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-2.2.7.tgz", @@ -25288,16 +25321,6 @@ "node": ">=8" } }, - "node_modules/lint-staged/node_modules/yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 14" - } - }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -26644,6 +26667,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi3-ts": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.5.0.tgz", + "integrity": "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==", + "license": "MIT", + "dependencies": { + "yaml": "^2.8.0" + } + }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -31835,6 +31867,30 @@ "url": "https://opencollective.com/svgo" } }, + "node_modules/swagger-ui-dist": { + "version": "5.28.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.28.1.tgz", + "integrity": "sha512-IvPrtNi8MvjiuDgoSmPYgg27Lvu38fnLD1OSd8Y103xXsPAqezVNnNeHnVCZ/d+CMXJblflGaIyHxAYIF3O71w==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/swc-loader": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.6.tgz", @@ -33727,6 +33783,18 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 0579f31665..320986d5da 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "author": "DataLens Team ", "license": "Apache-2.0", "dependencies": { + "@asteasolutions/zod-to-openapi": "^7.3.4", "@braintree/sanitize-url": "^6.0.0", "@datalens-tech/ui-sandbox-modules": "^0.36.0", "@datalens-tech/xlsx": "^0.20.1", @@ -118,6 +119,7 @@ "request-ip": "^3.3.0", "request-promise-native": "^1.0.9", "set-cookie-parser": "^2.7.1", + "swagger-ui-express": "^5.0.1", "workerpool": "^9.1.1", "zod": "^3.25.64" }, @@ -182,6 +184,7 @@ "@types/request-ip": "^0.0.41", "@types/request-promise-native": "^1.0.21", "@types/set-cookie-parser": "^2.4.10", + "@types/swagger-ui-express": "^4.1.8", "@types/uuid": "^9.0.8", "@types/webpack-env": "^1.16.0", "bem-cn-lite": "^4.0.0", diff --git a/src/server/components/public-api/utils/init-public-api-swagger.ts b/src/server/components/public-api/utils/init-public-api-swagger.ts index fd6eb8fc85..c3898f2204 100644 --- a/src/server/components/public-api/utils/init-public-api-swagger.ts +++ b/src/server/components/public-api/utils/init-public-api-swagger.ts @@ -1,10 +1,7 @@ -// eslint-disable-next-line import/no-extraneous-dependencies import {OpenAPIRegistry, OpenApiGeneratorV31} from '@asteasolutions/zod-to-openapi'; -// eslint-disable-next-line import/no-extraneous-dependencies import type {OpenAPIObjectConfigV31} from '@asteasolutions/zod-to-openapi/dist/v3.1/openapi-generator'; import type {ExpressKit} from '@gravity-ui/expresskit'; import {AppError} from '@gravity-ui/nodekit'; -// eslint-disable-next-line import/no-extraneous-dependencies import swaggerUi from 'swagger-ui-express'; import z from 'zod/v4'; diff --git a/src/server/registry/index.ts b/src/server/registry/index.ts index 0babce8eed..21f19cddb8 100644 --- a/src/server/registry/index.ts +++ b/src/server/registry/index.ts @@ -24,7 +24,6 @@ export const wrapperGetGatewayControllers = ( let gateway: ReturnType; let gatewaySchemasByScope: SchemasByScope; -let publicSchema: any; let getLayoutConfig: GetLayoutConfig | undefined; let yfmPlugins: MarkdownItPluginCb[]; let getXlsxConverter: XlsxConverterFn | undefined; @@ -60,13 +59,11 @@ export const registry = { setupGateway( config: GatewayConfig, schemasByScope: SchemasByScope, - publicSchemaArg?: any, // TODO @flops ) { if (gateway) { throw new Error('The method must not be called more than once'); } gateway = wrapperGetGatewayControllers(schemasByScope, config); - publicSchema = publicSchemaArg; gatewaySchemasByScope = schemasByScope; }, getGatewayController() { @@ -92,13 +89,6 @@ export const registry = { return gatewaySchemasByScope; }, - getPublicApi() { - if (!publicSchema) { - throw new Error('First of all setup the publicSchema'); - } - - return publicSchema; - }, registerGetLayoutConfig(fn: GetLayoutConfig) { if (getLayoutConfig) { throw new Error( From 224211117d96379ce621d2e86e33930a17280428 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Mon, 8 Sep 2025 11:13:54 +0300 Subject: [PATCH 13/40] Fixes --- src/server/components/public-api/constants.ts | 66 +++++++++++-------- src/server/registry/index.ts | 1 - 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/server/components/public-api/constants.ts b/src/server/components/public-api/constants.ts index bed035757b..7d40ef1e70 100644 --- a/src/server/components/public-api/constants.ts +++ b/src/server/components/public-api/constants.ts @@ -4,6 +4,16 @@ export const PUBLIC_API_HTTP_METHOD = 'POST'; export const PUBLIC_API_URL = '/rpc/:version/:action'; export const PUBLIC_API_ROUTE = `${PUBLIC_API_HTTP_METHOD} ${PUBLIC_API_URL}`; +enum ApiTag { + Navigation = 'Navigation', + Connection = 'Connection', + Dataset = 'Dataset', + Wizard = 'Wizard', + Editor = 'Editor', + Dashboard = 'Dashboard', + Report = 'Report', +} + export const PUBLIC_API_PROXY_MAP = { v0: { // navigation @@ -11,28 +21,28 @@ export const PUBLIC_API_PROXY_MAP = { resolve: (api) => api.mix.getNavigationList, openApi: { summary: 'Get navigation list', - tags: ['navigation'], + tags: [ApiTag.Navigation], }, }, getStructureItems: { resolve: (api) => api.us.getStructureItems, openApi: { summary: 'Get structure list', - tags: ['navigation'], + tags: [ApiTag.Navigation], }, }, createWorkbook: { resolve: (api) => api.us.createWorkbook, openApi: { summary: 'Create workbook', - tags: ['navigation'], + tags: [ApiTag.Navigation], }, }, createCollection: { resolve: (api) => api.us.createCollection, openApi: { summary: 'Create collection', - tags: ['navigation'], + tags: [ApiTag.Navigation], }, }, // connection @@ -40,28 +50,28 @@ export const PUBLIC_API_PROXY_MAP = { resolve: (api) => api.bi.getConnection, openApi: { summary: 'Get connection', - tags: ['connection'], + tags: [ApiTag.Connection], }, }, updateConnection: { resolve: (api) => api.bi.updateConnection, openApi: { summary: 'Update connection', - tags: ['connection'], + tags: [ApiTag.Connection], }, }, createConnection: { resolve: (api) => api.bi.createConnection, openApi: { summary: 'Create connection', - tags: ['connection'], + tags: [ApiTag.Connection], }, }, deleteConnection: { resolve: (api) => api.bi.deleteConnnection, openApi: { summary: 'Delete connection', - tags: ['connection'], + tags: [ApiTag.Connection], }, }, // dataset @@ -69,28 +79,28 @@ export const PUBLIC_API_PROXY_MAP = { resolve: (api) => api.bi.getDatasetApi, openApi: { summary: 'Get dataset', - tags: ['dataset'], + tags: [ApiTag.Dataset], }, }, updateDataset: { resolve: (api) => api.bi.updateDatasetApi, openApi: { summary: 'Update dataset', - tags: ['dataset'], + tags: [ApiTag.Dataset], }, }, createDataset: { resolve: (api) => api.bi.createDatasetApi, openApi: { summary: 'Create dataset', - tags: ['dataset'], + tags: [ApiTag.Dataset], }, }, deleteDataset: { resolve: (api) => api.bi.deleteDatasetApi, openApi: { summary: 'Delete dataset', - tags: ['dataset'], + tags: [ApiTag.Dataset], }, }, // wizard @@ -98,28 +108,28 @@ export const PUBLIC_API_PROXY_MAP = { resolve: (api) => api.mix.getWizardChartApi, openApi: { summary: 'Get wizard chart', - tags: ['wizard'], + tags: [ApiTag.Wizard], }, }, updateWizardChart: { resolve: (api) => api.mix.updateWizardChartApi, openApi: { summary: 'Update wizard chart', - tags: ['wizard'], + tags: [ApiTag.Wizard], }, }, createWizardChart: { resolve: (api) => api.mix.createWizardChartApi, openApi: { summary: 'Create wizard chart', - tags: ['wizard'], + tags: [ApiTag.Wizard], }, }, deleteWizardChart: { resolve: (api) => api.mix.deleteWizardChartApi, openApi: { summary: 'Delete wizard chart', - tags: ['wizard'], + tags: [ApiTag.Wizard], }, }, // editor @@ -127,28 +137,28 @@ export const PUBLIC_API_PROXY_MAP = { resolve: (api) => api.mix.getEditorChartApi, openApi: { summary: 'Get editor chart', - tags: ['editor'], + tags: [ApiTag.Editor], }, }, updateEditorChart: { resolve: (api) => api.mix.updateEditorChart, openApi: { summary: 'Update editor chart', - tags: ['editor'], + tags: [ApiTag.Editor], }, }, createEditorChart: { resolve: (api) => api.mix.createEditorChart, openApi: { summary: 'Create editor chart', - tags: ['editor'], + tags: [ApiTag.Editor], }, }, deleteEditorChart: { resolve: (api) => api.mix.deleteEditorChartApi, openApi: { summary: 'Delete editor chart', - tags: ['editor'], + tags: [ApiTag.Editor], }, }, // Dash @@ -156,28 +166,28 @@ export const PUBLIC_API_PROXY_MAP = { resolve: (api) => api.mix.getDashboardApi, openApi: { summary: 'Get dashboard', - tags: ['dashboard'], + tags: [ApiTag.Dashboard], }, }, updateDashboard: { resolve: (api) => api.mix.updateDashboardApi, openApi: { summary: 'Delete dashboard', - tags: ['dashboard'], + tags: [ApiTag.Dashboard], }, }, createDashboard: { resolve: (api) => api.mix.createDashboardApi, openApi: { summary: 'Create dashboard', - tags: ['dashboard'], + tags: [ApiTag.Dashboard], }, }, deleteDashboard: { resolve: (api) => api.mix.deleteDashboardApi, openApi: { summary: 'Delete dashboard', - tags: ['dashboard'], + tags: [ApiTag.Dashboard], }, }, // Report @@ -185,28 +195,28 @@ export const PUBLIC_API_PROXY_MAP = { // resolve: (api) => api.bi.createDataset, // openApi: { // summary: 'Get report', - // tags: ['report'], + // tags: [ApiTag.Report], // }, // }, // updateReport: { // resolve: (api) => api.bi.updateDataset, // openApi: { // summary: 'Delete report', - // tags: ['report'], + // tags: [ApiTag.Report], // }, // }, // createReport: { // resolve: (api) => api.bi.createDataset, // openApi: { // summary: 'Create report', - // tags: ['report'], + // tags: [ApiTag.Report], // }, // }, // deleteReport: { // resolve: (api) => api.bi.deleteDataset, // openApi: { // summary: 'Delete report', - // tags: ['report'], + // tags: [ApiTag.Report], // }, // }, }, diff --git a/src/server/registry/index.ts b/src/server/registry/index.ts index 21f19cddb8..003b951b93 100644 --- a/src/server/registry/index.ts +++ b/src/server/registry/index.ts @@ -3,7 +3,6 @@ import type {ExpressKit, Request, Response} from '@gravity-ui/expresskit'; import type {ApiWithRoot, GatewayConfig, SchemasByScope} from '@gravity-ui/gateway'; import {getGatewayControllers} from '@gravity-ui/gateway'; import type {AppContext} from '@gravity-ui/nodekit'; -import _ from 'lodash'; import type {ChartsEngine} from '../components/charts-engine'; import type {PublicApiConfig} from '../components/public-api/types'; From 23e96e0c9f5493221fb79559ddeefe6a9f59bda8 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Mon, 8 Sep 2025 14:21:46 +0300 Subject: [PATCH 14/40] Fixes --- src/server/components/api-docs/constants.ts | 1 + .../utils/init-public-api-swagger.ts | 53 ++++-- src/shared/schema/bi/actions/datasets.ts | 138 ++++++++------- src/shared/schema/gateway-utils.ts | 109 +++--------- src/shared/schema/mix/actions/dash.ts | 138 ++++++++------- src/shared/schema/mix/actions/editor.ts | 72 ++++---- src/shared/schema/mix/actions/wizard.ts | 160 ++++++++++-------- 7 files changed, 343 insertions(+), 328 deletions(-) create mode 100644 src/server/components/api-docs/constants.ts diff --git a/src/server/components/api-docs/constants.ts b/src/server/components/api-docs/constants.ts new file mode 100644 index 0000000000..6b1a01cad6 --- /dev/null +++ b/src/server/components/api-docs/constants.ts @@ -0,0 +1 @@ +export const CONTENT_TYPE_JSON = 'application/json'; diff --git a/src/server/components/public-api/utils/init-public-api-swagger.ts b/src/server/components/public-api/utils/init-public-api-swagger.ts index c3898f2204..a6271baa92 100644 --- a/src/server/components/public-api/utils/init-public-api-swagger.ts +++ b/src/server/components/public-api/utils/init-public-api-swagger.ts @@ -1,13 +1,16 @@ +import type {ZodMediaTypeObject} from '@asteasolutions/zod-to-openapi'; import {OpenAPIRegistry, OpenApiGeneratorV31} from '@asteasolutions/zod-to-openapi'; import type {OpenAPIObjectConfigV31} from '@asteasolutions/zod-to-openapi/dist/v3.1/openapi-generator'; import type {ExpressKit} from '@gravity-ui/expresskit'; import {AppError} from '@gravity-ui/nodekit'; import swaggerUi from 'swagger-ui-express'; -import z from 'zod/v4'; +import z from 'zod'; +import z4 from 'zod/v4'; -import {getValidationSchema, hasValidationSchema} from '../../../../shared/schema/gateway-utils'; +import {getValidationSchema} from '../../../../shared/schema/gateway-utils'; import {registry} from '../../../registry'; import type {DatalensGatewaySchemas} from '../../../types/gateway'; +import {CONTENT_TYPE_JSON} from '../../api-docs/constants'; import {PUBLIC_API_HTTP_METHOD, PUBLIC_API_URL} from '../constants'; import type {PublicApiSecuritySchemes} from '../types'; @@ -22,8 +25,8 @@ const defaultSchema = { request: { body: { content: { - ['application/json']: { - schema: z.toJSONSchema(z.any()), + [CONTENT_TYPE_JSON]: { + schema: z.object({}), }, }, }, @@ -32,8 +35,8 @@ const defaultSchema = { 200: { description: 'TBD', content: { - ['application/json']: { - schema: z.toJSONSchema(z.any()), + [CONTENT_TYPE_JSON]: { + schema: z.object({}), }, }, }, @@ -80,22 +83,50 @@ export const initPublicApiSwagger = ( const actionConfig = schemasByScope.root[pathObject.serviceName].actions[pathObject.actionName]; - if (hasValidationSchema(actionConfig)) { + const actionSchema = getValidationSchema(actionConfig); + + if (actionSchema) { publicApiOpenApiRegistry.registerPath({ - method: PUBLIC_API_HTTP_METHOD.toLocaleLowerCase(), + method: PUBLIC_API_HTTP_METHOD.toLocaleLowerCase() as Lowercase< + typeof PUBLIC_API_HTTP_METHOD + >, path: resolveUrl({version, action}), ...openApi, - ...getValidationSchema(actionConfig)().getOpenApiSchema(), + request: { + body: { + content: { + [CONTENT_TYPE_JSON]: { + schema: z4.toJSONSchema( + actionSchema.argsSchema, + ) as ZodMediaTypeObject['schema'], + }, + }, + }, + }, + responses: { + 200: { + description: 'Response', + content: { + [CONTENT_TYPE_JSON]: { + schema: z4.toJSONSchema( + actionSchema.bodySchema, + ) as ZodMediaTypeObject['schema'], + }, + }, + }, + }, security, }); } else { publicApiOpenApiRegistry.registerPath({ - method: PUBLIC_API_HTTP_METHOD.toLocaleLowerCase(), + method: PUBLIC_API_HTTP_METHOD.toLocaleLowerCase() as Lowercase< + typeof PUBLIC_API_HTTP_METHOD + >, path: resolveUrl({version, action}), ...openApi, ...defaultSchema, security, - } as any); + }); } }); }); diff --git a/src/shared/schema/bi/actions/datasets.ts b/src/shared/schema/bi/actions/datasets.ts index 9b0017d48b..1ea503940a 100644 --- a/src/shared/schema/bi/actions/datasets.ts +++ b/src/shared/schema/bi/actions/datasets.ts @@ -271,71 +271,83 @@ export const actions = { path: ({datasetId}) => `${API_V1}/datasets/${filterUrlFragment(datasetId)}`, params: (_, headers) => ({headers}), }), - createDatasetApi: createTypedAction({ - bodySchema: z.object({ - id: z.string(), - dataset: datasetBodySchema, - options: datasetOptionsSchema, - }), - argsSchema: createDatasetArgsSchema, - }).withValidationSchema({ - method: 'POST', - path: () => `${API_V1}/datasets`, - params: ({dataset, ...restBody}, headers, {ctx}) => { - const resultDataset = prepareDatasetProperty(ctx, dataset); - return {body: {...restBody, dataset: resultDataset}, headers}; + createDatasetApi: createTypedAction( + { + bodySchema: z.object({ + id: z.string(), + dataset: datasetBodySchema, + options: datasetOptionsSchema, + }), + argsSchema: createDatasetArgsSchema, }, - }), - updateDatasetApi: createTypedAction({ - bodySchema: z.object({ - id: z.string(), - dataset: datasetBodySchema, - options: datasetOptionsSchema, - }), - argsSchema: z.object({ - version: z.literal('draft'), - datasetId: z.string(), - multisource: z.boolean(), - dataset: datasetBodySchema, - }), - }).withValidationSchema({ - method: 'PUT', - path: ({datasetId, version}) => - `${API_V1}/datasets/${filterUrlFragment(datasetId)}/versions/${filterUrlFragment( - version, - )}`, - params: ({dataset, multisource}, headers, {ctx}) => { - const resultDataset = prepareDatasetProperty(ctx, dataset); - return {body: {dataset: resultDataset, multisource}, headers}; + { + method: 'POST', + path: () => `${API_V1}/datasets`, + params: ({dataset, ...restBody}, headers, {ctx}) => { + const resultDataset = prepareDatasetProperty(ctx, dataset); + return {body: {...restBody, dataset: resultDataset}, headers}; + }, }, - }), - deleteDatasetApi: createTypedAction({ - bodySchema: z.unknown(), - argsSchema: z.object({ - datasetId: z.string(), - }), - }).withValidationSchema({ - method: 'DELETE', - path: ({datasetId}) => `${API_V1}/datasets/${filterUrlFragment(datasetId)}`, - params: (_, headers) => ({headers}), - }), - getDatasetApi: createTypedAction({ - bodySchema: datasetSchema, - argsSchema: z.object({ - datasetId: z.string(), - version: z.literal('draft'), - workbookId: z.union([z.null(), z.string()]), - }), - }).withValidationSchema({ - method: 'GET', - path: ({datasetId, version}) => - `${API_V1}/datasets/${filterUrlFragment(datasetId)}/versions/${filterUrlFragment( - version, - )}`, - params: ({workbookId}, headers) => ({ - headers: {...(workbookId ? {[WORKBOOK_ID_HEADER]: workbookId} : {}), ...headers}, - }), - }), + ), + updateDatasetApi: createTypedAction( + { + bodySchema: z.object({ + id: z.string(), + dataset: datasetBodySchema, + options: datasetOptionsSchema, + }), + argsSchema: z.object({ + version: z.literal('draft'), + datasetId: z.string(), + multisource: z.boolean(), + dataset: datasetBodySchema, + }), + }, + { + method: 'PUT', + path: ({datasetId, version}) => + `${API_V1}/datasets/${filterUrlFragment(datasetId)}/versions/${filterUrlFragment( + version, + )}`, + params: ({dataset, multisource}, headers, {ctx}) => { + const resultDataset = prepareDatasetProperty(ctx, dataset); + return {body: {dataset: resultDataset, multisource}, headers}; + }, + }, + ), + deleteDatasetApi: createTypedAction( + { + bodySchema: z.unknown(), + argsSchema: z.object({ + datasetId: z.string(), + }), + }, + { + method: 'DELETE', + path: ({datasetId}) => `${API_V1}/datasets/${filterUrlFragment(datasetId)}`, + params: (_, headers) => ({headers}), + }, + ), + getDatasetApi: createTypedAction( + { + bodySchema: datasetSchema, + argsSchema: z.object({ + datasetId: z.string(), + version: z.literal('draft'), + workbookId: z.union([z.null(), z.string()]), + }), + }, + { + method: 'GET', + path: ({datasetId, version}) => + `${API_V1}/datasets/${filterUrlFragment(datasetId)}/versions/${filterUrlFragment( + version, + )}`, + params: ({workbookId}, headers) => ({ + headers: {...(workbookId ? {[WORKBOOK_ID_HEADER]: workbookId} : {}), ...headers}, + }), + }, + ), _proxyExportDataset: createAction({ method: 'POST', path: ({datasetId}) => `${API_V1}/datasets/export/${datasetId}`, diff --git a/src/shared/schema/gateway-utils.ts b/src/shared/schema/gateway-utils.ts index 57c323ec06..2c0b24d19b 100644 --- a/src/shared/schema/gateway-utils.ts +++ b/src/shared/schema/gateway-utils.ts @@ -1,12 +1,12 @@ import type {Request, Response} from '@gravity-ui/expresskit'; import type { ApiServiceActionConfig, - ApiServiceMixedActionConfig, - ApiServiceRestActionConfig, + // ApiServiceMixedActionConfig, + // ApiServiceRestActionConfig, GetAuthHeaders, } from '@gravity-ui/gateway'; import type {AppContext} from '@gravity-ui/nodekit'; -import z from 'zod/v4'; +import type z from 'zod/v4'; import {AuthHeader, SERVICE_USER_ACCESS_TOKEN_HEADER} from '../constants'; @@ -18,46 +18,32 @@ export function createAction, -): actionConfig is ApiServiceRestActionConfig { - return Boolean((actionConfig as ApiServiceRestActionConfig).method); -} - -function isMixedActionConfig( - actionConfig: ApiServiceActionConfig, -): actionConfig is ApiServiceMixedActionConfig { - return typeof actionConfig === 'function'; -} +type TypedActionSchema = { + argsSchema: z.ZodType; + bodySchema: z.ZodType; +}; const VALIDATION_SCHEMA_KEY = Symbol('$schema'); -export const registerValidationSchema = >( - actionConfig: T, - schema: any, -) => { - Object.defineProperty(actionConfig, VALIDATION_SCHEMA_KEY, { +const registerValidationSchema = (value: T, schema: TypedActionSchema): T => { + Object.defineProperty(value, VALIDATION_SCHEMA_KEY, { value: schema, enumerable: false, }); - return actionConfig; + return value; }; -export const hasValidationSchema = >( - actionConfig: T, -) => { - return Object.hasOwnProperty.call(actionConfig, VALIDATION_SCHEMA_KEY); +export const hasValidationSchema = ( + value: object, +): value is {[VALIDATION_SCHEMA_KEY]: TypedActionSchema} => { + return Object.prototype.hasOwnProperty.call(value, VALIDATION_SCHEMA_KEY); }; -export const getValidationSchema = >( - actionConfig: T, -) => { - return hasValidationSchema(actionConfig) ? (actionConfig as any)[VALIDATION_SCHEMA_KEY] : null; +export const getValidationSchema = (value: object): TypedActionSchema | null => { + return hasValidationSchema(value) ? value[VALIDATION_SCHEMA_KEY] : null; }; -const CONTENT_TYPE_JSON = 'application/json'; - export function createTypedAction< TOutputSchema extends z.ZodType, TParamsSchema extends z.ZodType, @@ -65,68 +51,23 @@ export function createTypedAction< TOutput = z.infer, TParams = z.infer, TTransformed = z.infer, ->(schema: {bodySchema: TOutputSchema; argsSchema: TParamsSchema}) { - type ActionConfig = ApiServiceActionConfig< +>( + schema: {bodySchema: TOutputSchema; argsSchema: TParamsSchema}, + actionConfig: ApiServiceActionConfig< AppContext, Request, Response, TOutput, TParams, TTransformed - >; - - const shemaValidationObject = () => ({ - getSchema() { - return schema; - }, - getOpenApiSchema() { - return { - request: { - body: { - content: { - [CONTENT_TYPE_JSON]: { - schema: z.toJSONSchema(schema.argsSchema), - }, - }, - }, - }, - responses: { - 200: { - description: 'Response', - content: { - [CONTENT_TYPE_JSON]: { - schema: z.toJSONSchema(schema.bodySchema), - }, - }, - }, - }, - }; - }, - }); - - const action = (actionConfig: ActionConfig) => - registerValidationSchema(actionConfig, shemaValidationObject); - - action.withValidationSchema = (actionConfig: ActionConfig) => { - if (isRestActionConfig(actionConfig)) { - return registerValidationSchema( - { - ...actionConfig, - validationSchema: z.toJSONSchema(schema.argsSchema, { - target: 'draft-7', - io: 'input', - }), - }, - shemaValidationObject, - ); - } else if (isMixedActionConfig(actionConfig)) { - return action(actionConfig); // TODO add validation - } - - return action(actionConfig); + >, +) { + const schemaValidationObject = { + argsSchema: schema.argsSchema, + bodySchema: schema.bodySchema, }; - return action; + return registerValidationSchema(actionConfig, schemaValidationObject); } type AuthArgsData = { diff --git a/src/shared/schema/mix/actions/dash.ts b/src/shared/schema/mix/actions/dash.ts index b6dfa53c58..6dc24a19dd 100644 --- a/src/shared/schema/mix/actions/dash.ts +++ b/src/shared/schema/mix/actions/dash.ts @@ -61,77 +61,89 @@ const dashUsUpdateSchema = z.object({ }); export const dashActions = { - getDashboardApi: createTypedAction({ - argsSchema: z.object({ - dashboardId: z.string(), - revId: z.string().optional(), - includePermissions: z.boolean().optional().default(false), - includeLinks: z.boolean().optional().default(false), - branch: z.literal(['published', 'saved']).optional().default('published'), - }), - bodySchema: dashUsSchema, - }).withValidationSchema(async (_, args, {headers, ctx}) => { - const {dashboardId, includePermissions, includeLinks, branch, revId} = args; + getDashboardApi: createTypedAction( + { + argsSchema: z.object({ + dashboardId: z.string(), + revId: z.string().optional(), + includePermissions: z.boolean().optional().default(false), + includeLinks: z.boolean().optional().default(false), + branch: z.literal(['published', 'saved']).optional().default('published'), + }), + bodySchema: dashUsSchema, + }, + async (_, args, {headers, ctx}) => { + const {dashboardId, includePermissions, includeLinks, branch, revId} = args; - if (!dashboardId || dashboardId === 'null') { - throw new Error(`Not found ${dashboardId} id`); - } + if (!dashboardId || dashboardId === 'null') { + throw new Error(`Not found ${dashboardId} id`); + } - const result = await Dash.read( - dashboardId, - { - includePermissions: includePermissions ? includePermissions?.toString() : '0', - includeLinks: includeLinks ? includeLinks?.toString() : '0', - ...(branch ? {branch} : {branch: 'published'}), - ...(revId ? {revId} : {}), - }, - headers, - ctx, - {forceMigrate: true}, - ); + const result = await Dash.read( + dashboardId, + { + includePermissions: includePermissions ? includePermissions?.toString() : '0', + includeLinks: includeLinks ? includeLinks?.toString() : '0', + ...(branch ? {branch} : {branch: 'published'}), + ...(revId ? {revId} : {}), + }, + headers, + ctx, + {forceMigrate: true}, + ); - if (result.scope !== EntryScope.Dash) { - throw new Error('No entry found'); - } + if (result.scope !== EntryScope.Dash) { + throw new Error('No entry found'); + } - return pick(result, DASH_ENTRY_RELEVANT_FIELDS) as any; - }), - deleteDashboardApi: createTypedAction({ - argsSchema: z.object({ - dashboardId: z.string(), - lockToken: z.string().optional(), - }), - bodySchema: z.any(), - }).withValidationSchema(async (api, {lockToken, dashboardId}) => { - const typedApi = getTypedApi(api); + return pick(result, DASH_ENTRY_RELEVANT_FIELDS) as any; + }, + ), + deleteDashboardApi: createTypedAction( + { + argsSchema: z.object({ + dashboardId: z.string(), + lockToken: z.string().optional(), + }), + bodySchema: z.any(), + }, + async (api, {lockToken, dashboardId}) => { + const typedApi = getTypedApi(api); - await typedApi.us._deleteUSEntry({ - entryId: dashboardId, - lockToken, - }); - }), - updateDashboardApi: createTypedAction({ - argsSchema: dashUsUpdateSchema, - bodySchema: dashUsSchema, - }).withValidationSchema(async (_, args, {headers, ctx}) => { - const {entryId} = args; + await typedApi.us._deleteUSEntry({ + entryId: dashboardId, + lockToken, + }); + }, + ), + updateDashboardApi: createTypedAction( + { + argsSchema: dashUsUpdateSchema, + bodySchema: dashUsSchema, + }, + async (_, args, {headers, ctx}) => { + const {entryId} = args; - const I18n = ctx.get('i18n'); + const I18n = ctx.get('i18n'); - return (await Dash.update(entryId as any, args as any, headers, ctx, I18n, { - forceMigrate: true, - })) as unknown as z.infer; - }), - createDashboardApi: createTypedAction({ - argsSchema: dashUsCreateSchema, - bodySchema: dashUsSchema, - }).withValidationSchema(async (_, args, {headers, ctx}) => { - const I18n = ctx.get('i18n'); + return (await Dash.update(entryId as any, args as any, headers, ctx, I18n, { + forceMigrate: true, + })) as unknown as z.infer; + }, + ), + createDashboardApi: createTypedAction( + { + argsSchema: dashUsCreateSchema, + bodySchema: dashUsSchema, + }, + async (_, args, {headers, ctx}) => { + const I18n = ctx.get('i18n'); - return (await Dash.create(args as any, headers, ctx, I18n)) as unknown as z.infer< - typeof dashUsSchema - >; - }), + return (await Dash.create(args as any, headers, ctx, I18n)) as unknown as z.infer< + typeof dashUsSchema + >; + }, + ), collectDashStats: createAction( async (_, args, {ctx}) => { diff --git a/src/shared/schema/mix/actions/editor.ts b/src/shared/schema/mix/actions/editor.ts index deea09add1..4d005c5ce4 100644 --- a/src/shared/schema/mix/actions/editor.ts +++ b/src/shared/schema/mix/actions/editor.ts @@ -48,29 +48,32 @@ const editorUsSchema = z.object({ }); export const editorActions = { - getEditorChartApi: createTypedAction({ - argsSchema: z.object({ - chardId: z.string(), - workbookId: z.union([z.string(), z.null()]).default(null).optional(), - revId: z.string().optional(), - includePermissions: z.boolean().default(false).optional(), - includeLinks: z.boolean().default(false).optional(), - branch: z.literal(['saved', 'published']).default('published').optional(), - }), - bodySchema: editorUsSchema, - }).withValidationSchema(async (api, args) => { - const {includePermissions, includeLinks, revId, chardId, branch, workbookId} = args; - const typedApi = getTypedApi(api); + getEditorChartApi: createTypedAction( + { + argsSchema: z.object({ + chardId: z.string(), + workbookId: z.union([z.string(), z.null()]).default(null).optional(), + revId: z.string().optional(), + includePermissions: z.boolean().default(false).optional(), + includeLinks: z.boolean().default(false).optional(), + branch: z.literal(['saved', 'published']).default('published').optional(), + }), + bodySchema: editorUsSchema, + }, + async (api, args) => { + const {includePermissions, includeLinks, revId, chardId, branch, workbookId} = args; + const typedApi = getTypedApi(api); - return typedApi.us.getEntry({ - entryId: chardId, - includePermissionsInfo: includePermissions ? Boolean(includePermissions) : false, - includeLinks: includeLinks ? Boolean(includeLinks) : false, - ...(revId ? {revId} : {}), - workbookId: workbookId || null, - branch: branch || 'published', - }) as unknown as z.infer; - }), + return typedApi.us.getEntry({ + entryId: chardId, + includePermissionsInfo: includePermissions ? Boolean(includePermissions) : false, + includeLinks: includeLinks ? Boolean(includeLinks) : false, + ...(revId ? {revId} : {}), + workbookId: workbookId || null, + branch: branch || 'published', + }) as unknown as z.infer; + }, + ), createEditorChart: createAction( async (api, args, {ctx}) => { const {checkRequestForDeveloperModeAccess} = ctx.get('gateway'); @@ -105,16 +108,19 @@ export const editorActions = { } }, ), - deleteEditorChartApi: createTypedAction({ - argsSchema: z.object({ - chartId: z.string(), - }), - bodySchema: z.any(), - }).withValidationSchema(async (api, {chartId}) => { - const typedApi = getTypedApi(api); + deleteEditorChartApi: createTypedAction( + { + argsSchema: z.object({ + chartId: z.string(), + }), + bodySchema: z.any(), + }, + async (api, {chartId}) => { + const typedApi = getTypedApi(api); - await typedApi.us._deleteUSEntry({ - entryId: chartId, - }); - }), + await typedApi.us._deleteUSEntry({ + entryId: chartId, + }); + }, + ), }; diff --git a/src/shared/schema/mix/actions/wizard.ts b/src/shared/schema/mix/actions/wizard.ts index 53b80ddeb0..da6456f804 100644 --- a/src/shared/schema/mix/actions/wizard.ts +++ b/src/shared/schema/mix/actions/wizard.ts @@ -27,85 +27,97 @@ const wizardUsSchema = z.object({ }); export const wizardActions = { - getWizardChartApi: createTypedAction({ - argsSchema: z.object({ - chardId: z.string(), - unreleased: z.boolean().default(false).optional(), - revId: z.string().optional(), - includePermissions: z.boolean().default(false).optional(), - includeLinks: z.boolean().default(false).optional(), - }), - bodySchema: wizardUsSchema, - }).withValidationSchema(async (_, args, {ctx, headers}) => { - const {includePermissions, includeLinks, unreleased, revId, chardId} = args; + getWizardChartApi: createTypedAction( + { + argsSchema: z.object({ + chardId: z.string(), + unreleased: z.boolean().default(false).optional(), + revId: z.string().optional(), + includePermissions: z.boolean().default(false).optional(), + includeLinks: z.boolean().default(false).optional(), + }), + bodySchema: wizardUsSchema, + }, + async (_, args, {ctx, headers}) => { + const {includePermissions, includeLinks, unreleased, revId, chardId} = args; - const result = await USProvider.retrieveParsedWizardChart(ctx, { - id: chardId, - includePermissionsInfo: includePermissions ? includePermissions?.toString() : '0', - includeLinks: includeLinks ? includeLinks?.toString() : '0', - ...(revId ? {revId} : {}), - ...(unreleased ? {unreleased} : {unreleased: false}), - headers, - }); + const result = await USProvider.retrieveParsedWizardChart(ctx, { + id: chardId, + includePermissionsInfo: includePermissions ? includePermissions?.toString() : '0', + includeLinks: includeLinks ? includeLinks?.toString() : '0', + ...(revId ? {revId} : {}), + ...(unreleased ? {unreleased} : {unreleased: false}), + headers, + }); - return result as any; - }), - createWizardChartApi: createTypedAction({ - argsSchema: z.object({ - entryId: z.string(), - data: v12ChartsConfigSchema, - key: z.string(), - workbookId: z.union([z.string(), z.null()]).optional(), - type: z.enum(WizardType).optional(), - name: z.string(), - }), - bodySchema: wizardUsSchema, - }).withValidationSchema(async (_, args, {ctx, headers}) => { - const {data, type, key, workbookId, name} = args; + return result as any; + }, + ), + createWizardChartApi: createTypedAction( + { + argsSchema: z.object({ + entryId: z.string(), + data: v12ChartsConfigSchema, + key: z.string(), + workbookId: z.union([z.string(), z.null()]).optional(), + type: z.enum(WizardType).optional(), + name: z.string(), + }), + bodySchema: wizardUsSchema, + }, + async (_, args, {ctx, headers}) => { + const {data, type, key, workbookId, name} = args; - const result = await USProvider.create(ctx, { - type, - data, - key, - name, - scope: EntryScope.Widget, - ...(workbookId ? {workbookId} : {workbookId: null}), - headers, - }); + const result = await USProvider.create(ctx, { + type, + data, + key, + name, + scope: EntryScope.Widget, + ...(workbookId ? {workbookId} : {workbookId: null}), + headers, + }); - return result as any; - }), - updateWizardChartApi: createTypedAction({ - argsSchema: z.object({ - entryId: z.string(), - revId: z.string().optional(), - data: v12ChartsConfigSchema, - type: z.enum(WizardType).optional(), - }), - bodySchema: wizardUsSchema, - }).withValidationSchema(async (_, args, {ctx, headers}) => { - const {entryId, revId, data, type} = args; + return result as any; + }, + ), + updateWizardChartApi: createTypedAction( + { + argsSchema: z.object({ + entryId: z.string(), + revId: z.string().optional(), + data: v12ChartsConfigSchema, + type: z.enum(WizardType).optional(), + }), + bodySchema: wizardUsSchema, + }, + async (_, args, {ctx, headers}) => { + const {entryId, revId, data, type} = args; - const result = await USProvider.update(ctx, { - entryId, - ...(revId ? {revId} : {}), - ...(type ? {type} : {}), - data, - headers, - }); + const result = await USProvider.update(ctx, { + entryId, + ...(revId ? {revId} : {}), + ...(type ? {type} : {}), + data, + headers, + }); - return result as any; - }), - deleteWizardChartApi: createTypedAction({ - argsSchema: z.object({ - chartId: z.string(), - }), - bodySchema: z.any(), - }).withValidationSchema(async (api, {chartId}) => { - const typedApi = getTypedApi(api); + return result as any; + }, + ), + deleteWizardChartApi: createTypedAction( + { + argsSchema: z.object({ + chartId: z.string(), + }), + bodySchema: z.any(), + }, + async (api, {chartId}) => { + const typedApi = getTypedApi(api); - await typedApi.us._deleteUSEntry({ - entryId: chartId, - }); - }), + await typedApi.us._deleteUSEntry({ + entryId: chartId, + }); + }, + ), }; From d98cb398b245f3dd3188ae9e514c2a808d8ddbe0 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Mon, 8 Sep 2025 16:57:06 +0300 Subject: [PATCH 15/40] Fixes --- .../utils/init-public-api-swagger.ts | 4 +- src/server/controllers/public-api.ts | 92 ++++++++++++++++--- src/shared/schema/bi/actions/datasets.ts | 24 ++--- src/shared/schema/gateway-utils.ts | 10 +- src/shared/schema/mix/actions/dash.ts | 16 ++-- src/shared/schema/mix/actions/editor.ts | 8 +- src/shared/schema/mix/actions/wizard.ts | 16 ++-- 7 files changed, 117 insertions(+), 53 deletions(-) diff --git a/src/server/components/public-api/utils/init-public-api-swagger.ts b/src/server/components/public-api/utils/init-public-api-swagger.ts index a6271baa92..911b1d0738 100644 --- a/src/server/components/public-api/utils/init-public-api-swagger.ts +++ b/src/server/components/public-api/utils/init-public-api-swagger.ts @@ -97,7 +97,7 @@ export const initPublicApiSwagger = ( content: { [CONTENT_TYPE_JSON]: { schema: z4.toJSONSchema( - actionSchema.argsSchema, + actionSchema.paramsSchema, ) as ZodMediaTypeObject['schema'], }, }, @@ -109,7 +109,7 @@ export const initPublicApiSwagger = ( content: { [CONTENT_TYPE_JSON]: { schema: z4.toJSONSchema( - actionSchema.bodySchema, + actionSchema.resultSchema, ) as ZodMediaTypeObject['schema'], }, }, diff --git a/src/server/controllers/public-api.ts b/src/server/controllers/public-api.ts index eb7f499234..7f81053a00 100644 --- a/src/server/controllers/public-api.ts +++ b/src/server/controllers/public-api.ts @@ -1,26 +1,66 @@ import type {Request, Response} from '@gravity-ui/expresskit'; -import {REQUEST_ID_PARAM_NAME} from '@gravity-ui/nodekit'; +import type {ApiServiceActionConfig} from '@gravity-ui/gateway'; +import {AppError, REQUEST_ID_PARAM_NAME} from '@gravity-ui/nodekit'; import _ from 'lodash'; +import {ZodError} from 'zod/v4'; +import {getValidationSchema} from '../../shared/schema/gateway-utils'; import type {PublicApiRpcMap} from '../components/public-api/types'; import {PUBLIC_API_RPC_ERROR_CODE} from '../constants/public-api'; import {registry} from '../registry'; import type {DatalensGatewaySchemas} from '../types/gateway'; import Utils from '../utils'; -const handleError = (req: Request, res: Response, status: number, message: string) => { +const handleError = ( + req: Request, + res: Response, + status: number, + message: string, + details?: unknown, +) => { res.status(status).send({ status, code: PUBLIC_API_RPC_ERROR_CODE, message, requestId: req.ctx.get(REQUEST_ID_PARAM_NAME) || '', + details, }); }; -export function createPublicApiController() { +export const createPublicApiController = () => { const {gatewayApi} = registry.getGatewayApi(); + const schemasByScope = registry.getGatewaySchemasByScope(); const {proxyMap} = registry.getPublicApiConfig(); + const actionToPathMap = new Map(); + + Object.entries(gatewayApi).forEach(([serviceName, actions]) => { + Object.entries(actions).forEach(([actionName, action]) => { + actionToPathMap.set(action, {serviceName, actionName}); + }); + }); + + const actionToConfigMap = new Map< + Function, + ApiServiceActionConfig + >(); + + Object.values(proxyMap).forEach((actions) => { + Object.values(actions).forEach(({resolve}) => { + const gatewayAction = resolve(gatewayApi); + const pathObject = actionToPathMap.get(gatewayAction); + + if (!pathObject) { + throw new AppError('Public api proxyMap action not found in gatewayApi.'); + } + + const actionConfig = + schemasByScope.root[pathObject.serviceName].actions[pathObject.actionName]; + + actionToConfigMap.set(gatewayAction, actionConfig); + }); + }); + return async function publicApiController(req: Request, res: Response) { const boundeHandler = handleError.bind(null, req, res); @@ -43,19 +83,43 @@ export function createPublicApiController() { const action = versionMap[actionName]; const {ctx} = req; - const initialHeaders = Utils.pickRpcHeaders(req); - const headers = action.headers ? action.headers(req, initialHeaders) : initialHeaders; - const args = action.args ? await action.args(req) : req.body; + const headers = Utils.pickRpcHeaders(req); + const args = req.body; const requestId = ctx.get(REQUEST_ID_PARAM_NAME) || ''; - const result = await action.resolve(gatewayApi)({ - headers, - args, - ctx, - requestId, - }); + const gatewayAction = action.resolve(gatewayApi); + const gatewayActionConfig = actionToConfigMap.get(gatewayAction); - res.status(200).send(result.responseData); + if (!gatewayActionConfig) { + return boundeHandler(404, 'Action not found'); + } + + const validationSchema = getValidationSchema(gatewayActionConfig); + + if (!validationSchema) { + return boundeHandler(404, 'Validation schema not found'); + } + + const {paramsSchema} = validationSchema; + + try { + const validatedArgs = await paramsSchema.parseAsync(args); + + const result = await gatewayAction({ + headers, + args: validatedArgs, + ctx, + requestId, + }); + + res.status(200).send(result.responseData); + } catch (error) { + if (error instanceof ZodError) { + return boundeHandler(400, 'Validation error', error.issues); + } else { + throw error; + } + } } catch (err) { const {error} = err as any; if (error) { @@ -65,4 +129,4 @@ export function createPublicApiController() { } } }; -} +}; diff --git a/src/shared/schema/bi/actions/datasets.ts b/src/shared/schema/bi/actions/datasets.ts index 1ea503940a..c2af6af774 100644 --- a/src/shared/schema/bi/actions/datasets.ts +++ b/src/shared/schema/bi/actions/datasets.ts @@ -273,12 +273,12 @@ export const actions = { }), createDatasetApi: createTypedAction( { - bodySchema: z.object({ + paramsSchema: createDatasetArgsSchema, + resultSchema: z.object({ id: z.string(), dataset: datasetBodySchema, options: datasetOptionsSchema, }), - argsSchema: createDatasetArgsSchema, }, { method: 'POST', @@ -291,17 +291,17 @@ export const actions = { ), updateDatasetApi: createTypedAction( { - bodySchema: z.object({ - id: z.string(), - dataset: datasetBodySchema, - options: datasetOptionsSchema, - }), - argsSchema: z.object({ + paramsSchema: z.object({ version: z.literal('draft'), datasetId: z.string(), multisource: z.boolean(), dataset: datasetBodySchema, }), + resultSchema: z.object({ + id: z.string(), + dataset: datasetBodySchema, + options: datasetOptionsSchema, + }), }, { method: 'PUT', @@ -317,10 +317,10 @@ export const actions = { ), deleteDatasetApi: createTypedAction( { - bodySchema: z.unknown(), - argsSchema: z.object({ + paramsSchema: z.object({ datasetId: z.string(), }), + resultSchema: z.unknown(), }, { method: 'DELETE', @@ -330,12 +330,12 @@ export const actions = { ), getDatasetApi: createTypedAction( { - bodySchema: datasetSchema, - argsSchema: z.object({ + paramsSchema: z.object({ datasetId: z.string(), version: z.literal('draft'), workbookId: z.union([z.null(), z.string()]), }), + resultSchema: datasetSchema, }, { method: 'GET', diff --git a/src/shared/schema/gateway-utils.ts b/src/shared/schema/gateway-utils.ts index 2c0b24d19b..814f7f8f0d 100644 --- a/src/shared/schema/gateway-utils.ts +++ b/src/shared/schema/gateway-utils.ts @@ -19,8 +19,8 @@ export function createAction, TTransformed = z.infer, >( - schema: {bodySchema: TOutputSchema; argsSchema: TParamsSchema}, + schema: {paramsSchema: TParamsSchema; resultSchema: TOutputSchema}, actionConfig: ApiServiceActionConfig< AppContext, Request, @@ -63,8 +63,8 @@ export function createTypedAction< >, ) { const schemaValidationObject = { - argsSchema: schema.argsSchema, - bodySchema: schema.bodySchema, + paramsSchema: schema.paramsSchema, + resultSchema: schema.resultSchema, }; return registerValidationSchema(actionConfig, schemaValidationObject); diff --git a/src/shared/schema/mix/actions/dash.ts b/src/shared/schema/mix/actions/dash.ts index 6dc24a19dd..84f31d6d7b 100644 --- a/src/shared/schema/mix/actions/dash.ts +++ b/src/shared/schema/mix/actions/dash.ts @@ -63,14 +63,14 @@ const dashUsUpdateSchema = z.object({ export const dashActions = { getDashboardApi: createTypedAction( { - argsSchema: z.object({ + paramsSchema: z.object({ dashboardId: z.string(), revId: z.string().optional(), includePermissions: z.boolean().optional().default(false), includeLinks: z.boolean().optional().default(false), branch: z.literal(['published', 'saved']).optional().default('published'), }), - bodySchema: dashUsSchema, + resultSchema: dashUsSchema, }, async (_, args, {headers, ctx}) => { const {dashboardId, includePermissions, includeLinks, branch, revId} = args; @@ -101,11 +101,11 @@ export const dashActions = { ), deleteDashboardApi: createTypedAction( { - argsSchema: z.object({ + paramsSchema: z.object({ dashboardId: z.string(), lockToken: z.string().optional(), }), - bodySchema: z.any(), + resultSchema: z.any(), }, async (api, {lockToken, dashboardId}) => { const typedApi = getTypedApi(api); @@ -118,8 +118,8 @@ export const dashActions = { ), updateDashboardApi: createTypedAction( { - argsSchema: dashUsUpdateSchema, - bodySchema: dashUsSchema, + paramsSchema: dashUsUpdateSchema, + resultSchema: dashUsSchema, }, async (_, args, {headers, ctx}) => { const {entryId} = args; @@ -133,8 +133,8 @@ export const dashActions = { ), createDashboardApi: createTypedAction( { - argsSchema: dashUsCreateSchema, - bodySchema: dashUsSchema, + paramsSchema: dashUsCreateSchema, + resultSchema: dashUsSchema, }, async (_, args, {headers, ctx}) => { const I18n = ctx.get('i18n'); diff --git a/src/shared/schema/mix/actions/editor.ts b/src/shared/schema/mix/actions/editor.ts index 4d005c5ce4..6f14aaa3e0 100644 --- a/src/shared/schema/mix/actions/editor.ts +++ b/src/shared/schema/mix/actions/editor.ts @@ -50,7 +50,7 @@ const editorUsSchema = z.object({ export const editorActions = { getEditorChartApi: createTypedAction( { - argsSchema: z.object({ + paramsSchema: z.object({ chardId: z.string(), workbookId: z.union([z.string(), z.null()]).default(null).optional(), revId: z.string().optional(), @@ -58,7 +58,7 @@ export const editorActions = { includeLinks: z.boolean().default(false).optional(), branch: z.literal(['saved', 'published']).default('published').optional(), }), - bodySchema: editorUsSchema, + resultSchema: editorUsSchema, }, async (api, args) => { const {includePermissions, includeLinks, revId, chardId, branch, workbookId} = args; @@ -110,10 +110,10 @@ export const editorActions = { ), deleteEditorChartApi: createTypedAction( { - argsSchema: z.object({ + paramsSchema: z.object({ chartId: z.string(), }), - bodySchema: z.any(), + resultSchema: z.any(), }, async (api, {chartId}) => { const typedApi = getTypedApi(api); diff --git a/src/shared/schema/mix/actions/wizard.ts b/src/shared/schema/mix/actions/wizard.ts index da6456f804..075c9307a9 100644 --- a/src/shared/schema/mix/actions/wizard.ts +++ b/src/shared/schema/mix/actions/wizard.ts @@ -29,14 +29,14 @@ const wizardUsSchema = z.object({ export const wizardActions = { getWizardChartApi: createTypedAction( { - argsSchema: z.object({ + paramsSchema: z.object({ chardId: z.string(), unreleased: z.boolean().default(false).optional(), revId: z.string().optional(), includePermissions: z.boolean().default(false).optional(), includeLinks: z.boolean().default(false).optional(), }), - bodySchema: wizardUsSchema, + resultSchema: wizardUsSchema, }, async (_, args, {ctx, headers}) => { const {includePermissions, includeLinks, unreleased, revId, chardId} = args; @@ -55,7 +55,7 @@ export const wizardActions = { ), createWizardChartApi: createTypedAction( { - argsSchema: z.object({ + paramsSchema: z.object({ entryId: z.string(), data: v12ChartsConfigSchema, key: z.string(), @@ -63,7 +63,7 @@ export const wizardActions = { type: z.enum(WizardType).optional(), name: z.string(), }), - bodySchema: wizardUsSchema, + resultSchema: wizardUsSchema, }, async (_, args, {ctx, headers}) => { const {data, type, key, workbookId, name} = args; @@ -83,13 +83,13 @@ export const wizardActions = { ), updateWizardChartApi: createTypedAction( { - argsSchema: z.object({ + paramsSchema: z.object({ entryId: z.string(), revId: z.string().optional(), data: v12ChartsConfigSchema, type: z.enum(WizardType).optional(), }), - bodySchema: wizardUsSchema, + resultSchema: wizardUsSchema, }, async (_, args, {ctx, headers}) => { const {entryId, revId, data, type} = args; @@ -107,10 +107,10 @@ export const wizardActions = { ), deleteWizardChartApi: createTypedAction( { - argsSchema: z.object({ + paramsSchema: z.object({ chartId: z.string(), }), - bodySchema: z.any(), + resultSchema: z.any(), }, async (api, {chartId}) => { const typedApi = getTypedApi(api); From f99ffe73c8e9028d93832b163c570e128ce1ff6d Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Tue, 9 Sep 2025 11:48:08 +0300 Subject: [PATCH 16/40] Fixes --- src/server/components/public-api/constants.ts | 76 ++++------- src/server/components/public-api/index.ts | 2 +- .../components/public-api/utils/index.ts | 3 +- .../utils/init-public-api-swagger.ts | 118 +----------------- .../utils/register-action-to-open-api.ts | 103 +++++++++++++++ src/server/controllers/public-api.ts | 15 ++- src/server/types/gateway.ts | 4 + 7 files changed, 144 insertions(+), 177 deletions(-) create mode 100644 src/server/components/public-api/utils/register-action-to-open-api.ts diff --git a/src/server/components/public-api/constants.ts b/src/server/components/public-api/constants.ts index 7d40ef1e70..81519f6fee 100644 --- a/src/server/components/public-api/constants.ts +++ b/src/server/components/public-api/constants.ts @@ -1,5 +1,9 @@ +import {OpenAPIRegistry} from '@asteasolutions/zod-to-openapi'; + import type {PublicApiRpcMap} from './types'; +export const publicApiOpenApiRegistry = new OpenAPIRegistry(); + export const PUBLIC_API_HTTP_METHOD = 'POST'; export const PUBLIC_API_URL = '/rpc/:version/:action'; export const PUBLIC_API_ROUTE = `${PUBLIC_API_HTTP_METHOD} ${PUBLIC_API_URL}`; @@ -11,7 +15,6 @@ enum ApiTag { Wizard = 'Wizard', Editor = 'Editor', Dashboard = 'Dashboard', - Report = 'Report', } export const PUBLIC_API_PROXY_MAP = { @@ -24,27 +27,27 @@ export const PUBLIC_API_PROXY_MAP = { tags: [ApiTag.Navigation], }, }, - getStructureItems: { - resolve: (api) => api.us.getStructureItems, - openApi: { - summary: 'Get structure list', - tags: [ApiTag.Navigation], - }, - }, - createWorkbook: { - resolve: (api) => api.us.createWorkbook, - openApi: { - summary: 'Create workbook', - tags: [ApiTag.Navigation], - }, - }, - createCollection: { - resolve: (api) => api.us.createCollection, - openApi: { - summary: 'Create collection', - tags: [ApiTag.Navigation], - }, - }, + // getStructureItems: { + // resolve: (api) => api.us.getStructureItems, + // openApi: { + // summary: 'Get structure list', + // tags: [ApiTag.Navigation], + // }, + // }, + // createWorkbook: { + // resolve: (api) => api.us.createWorkbook, + // openApi: { + // summary: 'Create workbook', + // tags: [ApiTag.Navigation], + // }, + // }, + // createCollection: { + // resolve: (api) => api.us.createCollection, + // openApi: { + // summary: 'Create collection', + // tags: [ApiTag.Navigation], + // }, + // }, // connection getConnection: { resolve: (api) => api.bi.getConnection, @@ -190,34 +193,5 @@ export const PUBLIC_API_PROXY_MAP = { tags: [ApiTag.Dashboard], }, }, - // Report - // getReport: { - // resolve: (api) => api.bi.createDataset, - // openApi: { - // summary: 'Get report', - // tags: [ApiTag.Report], - // }, - // }, - // updateReport: { - // resolve: (api) => api.bi.updateDataset, - // openApi: { - // summary: 'Delete report', - // tags: [ApiTag.Report], - // }, - // }, - // createReport: { - // resolve: (api) => api.bi.createDataset, - // openApi: { - // summary: 'Create report', - // tags: [ApiTag.Report], - // }, - // }, - // deleteReport: { - // resolve: (api) => api.bi.deleteDataset, - // openApi: { - // summary: 'Delete report', - // tags: [ApiTag.Report], - // }, - // }, }, } satisfies PublicApiRpcMap; diff --git a/src/server/components/public-api/index.ts b/src/server/components/public-api/index.ts index 79a123038b..e09e300a19 100644 --- a/src/server/components/public-api/index.ts +++ b/src/server/components/public-api/index.ts @@ -4,4 +4,4 @@ export { PUBLIC_API_ROUTE, PUBLIC_API_URL, } from './constants'; -export {initPublicApiSwagger, publicApiOpenApiRegistry} from './utils'; +export {initPublicApiSwagger, registerActionToOpenApi} from './utils'; diff --git a/src/server/components/public-api/utils/index.ts b/src/server/components/public-api/utils/index.ts index ad6e980fb4..435db20399 100644 --- a/src/server/components/public-api/utils/index.ts +++ b/src/server/components/public-api/utils/index.ts @@ -1 +1,2 @@ -export {initPublicApiSwagger, publicApiOpenApiRegistry} from './init-public-api-swagger'; +export {initPublicApiSwagger} from './init-public-api-swagger'; +export {registerActionToOpenApi} from './register-action-to-open-api'; diff --git a/src/server/components/public-api/utils/init-public-api-swagger.ts b/src/server/components/public-api/utils/init-public-api-swagger.ts index 911b1d0738..85e2ae1c16 100644 --- a/src/server/components/public-api/utils/init-public-api-swagger.ts +++ b/src/server/components/public-api/utils/init-public-api-swagger.ts @@ -1,48 +1,11 @@ -import type {ZodMediaTypeObject} from '@asteasolutions/zod-to-openapi'; -import {OpenAPIRegistry, OpenApiGeneratorV31} from '@asteasolutions/zod-to-openapi'; +import {OpenApiGeneratorV31} from '@asteasolutions/zod-to-openapi'; import type {OpenAPIObjectConfigV31} from '@asteasolutions/zod-to-openapi/dist/v3.1/openapi-generator'; import type {ExpressKit} from '@gravity-ui/expresskit'; -import {AppError} from '@gravity-ui/nodekit'; import swaggerUi from 'swagger-ui-express'; -import z from 'zod'; -import z4 from 'zod/v4'; -import {getValidationSchema} from '../../../../shared/schema/gateway-utils'; -import {registry} from '../../../registry'; -import type {DatalensGatewaySchemas} from '../../../types/gateway'; -import {CONTENT_TYPE_JSON} from '../../api-docs/constants'; -import {PUBLIC_API_HTTP_METHOD, PUBLIC_API_URL} from '../constants'; +import {publicApiOpenApiRegistry} from '../constants'; import type {PublicApiSecuritySchemes} from '../types'; -export const publicApiOpenApiRegistry = new OpenAPIRegistry(); - -const resolveUrl = ({version, action}: {version: string; action: string}) => { - return PUBLIC_API_URL.replace(':version', version).replace(':action', action); -}; - -const defaultSchema = { - summary: 'Type not defined', - request: { - body: { - content: { - [CONTENT_TYPE_JSON]: { - schema: z.object({}), - }, - }, - }, - }, - responses: { - 200: { - description: 'TBD', - content: { - [CONTENT_TYPE_JSON]: { - schema: z.object({}), - }, - }, - }, - }, -}; - export const initPublicApiSwagger = ( app: ExpressKit, securitySchemes?: PublicApiSecuritySchemes, @@ -54,83 +17,6 @@ export const initPublicApiSwagger = ( const descriptionText = `
Datalens api.`; setImmediate(() => { - const {gatewayApi} = registry.getGatewayApi(); - const schemasByScope = registry.getGatewaySchemasByScope(); - const {proxyMap, securityTypes} = registry.getPublicApiConfig(); - - const actionToPathMap = new Map(); - - Object.entries(gatewayApi).forEach(([serviceName, actions]) => { - Object.entries(actions).forEach(([actionName, action]) => { - actionToPathMap.set(action, {serviceName, actionName}); - }); - }); - - const security = securityTypes.map((type) => ({ - [type]: [], - })); - - Object.entries(proxyMap).forEach(([version, actions]) => { - Object.entries(actions).forEach(([action, {resolve, openApi}]) => { - const gatewayApiAction = resolve(gatewayApi); - - const pathObject = actionToPathMap.get(gatewayApiAction); - - if (!pathObject) { - throw new AppError('Public api proxyMap action not found in gatewayApi.'); - } - - const actionConfig = - schemasByScope.root[pathObject.serviceName].actions[pathObject.actionName]; - - const actionSchema = getValidationSchema(actionConfig); - - if (actionSchema) { - publicApiOpenApiRegistry.registerPath({ - method: PUBLIC_API_HTTP_METHOD.toLocaleLowerCase() as Lowercase< - typeof PUBLIC_API_HTTP_METHOD - >, - path: resolveUrl({version, action}), - ...openApi, - request: { - body: { - content: { - [CONTENT_TYPE_JSON]: { - schema: z4.toJSONSchema( - actionSchema.paramsSchema, - ) as ZodMediaTypeObject['schema'], - }, - }, - }, - }, - responses: { - 200: { - description: 'Response', - content: { - [CONTENT_TYPE_JSON]: { - schema: z4.toJSONSchema( - actionSchema.resultSchema, - ) as ZodMediaTypeObject['schema'], - }, - }, - }, - }, - security, - }); - } else { - publicApiOpenApiRegistry.registerPath({ - method: PUBLIC_API_HTTP_METHOD.toLocaleLowerCase() as Lowercase< - typeof PUBLIC_API_HTTP_METHOD - >, - path: resolveUrl({version, action}), - ...openApi, - ...defaultSchema, - security, - }); - } - }); - }); - if (securitySchemes) { Object.keys(securitySchemes).forEach((securityType) => { publicApiOpenApiRegistry.registerComponent('securitySchemes', securityType, { diff --git a/src/server/components/public-api/utils/register-action-to-open-api.ts b/src/server/components/public-api/utils/register-action-to-open-api.ts new file mode 100644 index 0000000000..ab33d7ab8c --- /dev/null +++ b/src/server/components/public-api/utils/register-action-to-open-api.ts @@ -0,0 +1,103 @@ +import type {ZodMediaTypeObject} from '@asteasolutions/zod-to-openapi'; +import z from 'zod'; +import z4 from 'zod/v4'; + +import {getValidationSchema} from '../../../../shared/schema/gateway-utils'; +import {registry} from '../../../registry'; +import type {AnyApiServiceActionConfig} from '../../../types/gateway'; +import {CONTENT_TYPE_JSON} from '../../api-docs/constants'; +import {PUBLIC_API_HTTP_METHOD, PUBLIC_API_URL, publicApiOpenApiRegistry} from '../constants'; + +const resolveUrl = ({version, actionName}: {version: string; actionName: string}) => { + return PUBLIC_API_URL.replace(':version', version).replace(':action', actionName); +}; + +const defaultSchema = { + summary: 'Type not defined', + request: { + body: { + content: { + [CONTENT_TYPE_JSON]: { + schema: z.object({}), + }, + }, + }, + }, + responses: { + 200: { + description: 'TBD', + content: { + [CONTENT_TYPE_JSON]: { + schema: z.object({}), + }, + }, + }, + }, +}; + +export const registerActionToOpenApi = ({ + actionConfig, + version, + actionName, + openApi, +}: { + actionConfig: AnyApiServiceActionConfig; + version: string; + actionName: string; + openApi: { + summary: string; + tags?: string[]; + }; +}) => { + const {securityTypes} = registry.getPublicApiConfig(); + + const actionSchema = getValidationSchema(actionConfig); + + const security = securityTypes.map((type) => ({ + [type]: [], + })); + + if (actionSchema) { + publicApiOpenApiRegistry.registerPath({ + method: PUBLIC_API_HTTP_METHOD.toLocaleLowerCase() as Lowercase< + typeof PUBLIC_API_HTTP_METHOD + >, + path: resolveUrl({version, actionName}), + ...openApi, + request: { + body: { + content: { + [CONTENT_TYPE_JSON]: { + schema: z4.toJSONSchema( + actionSchema.paramsSchema, + ) as ZodMediaTypeObject['schema'], + }, + }, + }, + }, + responses: { + 200: { + description: 'Response', + content: { + [CONTENT_TYPE_JSON]: { + schema: z4.toJSONSchema( + actionSchema.resultSchema, + ) as ZodMediaTypeObject['schema'], + }, + }, + }, + }, + security, + }); + } else { + publicApiOpenApiRegistry.registerPath({ + method: PUBLIC_API_HTTP_METHOD.toLocaleLowerCase() as Lowercase< + typeof PUBLIC_API_HTTP_METHOD + >, + path: resolveUrl({version, actionName}), + ...openApi, + ...defaultSchema, + security, + }); + } +}; diff --git a/src/server/controllers/public-api.ts b/src/server/controllers/public-api.ts index 7f81053a00..345b1ba350 100644 --- a/src/server/controllers/public-api.ts +++ b/src/server/controllers/public-api.ts @@ -1,14 +1,14 @@ import type {Request, Response} from '@gravity-ui/expresskit'; -import type {ApiServiceActionConfig} from '@gravity-ui/gateway'; import {AppError, REQUEST_ID_PARAM_NAME} from '@gravity-ui/nodekit'; import _ from 'lodash'; import {ZodError} from 'zod/v4'; import {getValidationSchema} from '../../shared/schema/gateway-utils'; +import {registerActionToOpenApi} from '../components/public-api'; import type {PublicApiRpcMap} from '../components/public-api/types'; import {PUBLIC_API_RPC_ERROR_CODE} from '../constants/public-api'; import {registry} from '../registry'; -import type {DatalensGatewaySchemas} from '../types/gateway'; +import type {AnyApiServiceActionConfig, DatalensGatewaySchemas} from '../types/gateway'; import Utils from '../utils'; const handleError = ( @@ -40,13 +40,10 @@ export const createPublicApiController = () => { }); }); - const actionToConfigMap = new Map< - Function, - ApiServiceActionConfig - >(); + const actionToConfigMap = new Map(); - Object.values(proxyMap).forEach((actions) => { - Object.values(actions).forEach(({resolve}) => { + Object.entries(proxyMap).forEach(([version, actions]) => { + Object.entries(actions).forEach(([actionName, {resolve, openApi}]) => { const gatewayAction = resolve(gatewayApi); const pathObject = actionToPathMap.get(gatewayAction); @@ -58,6 +55,8 @@ export const createPublicApiController = () => { schemasByScope.root[pathObject.serviceName].actions[pathObject.actionName]; actionToConfigMap.set(gatewayAction, actionConfig); + + registerActionToOpenApi({actionConfig, actionName, version, openApi}); }); }); diff --git a/src/server/types/gateway.ts b/src/server/types/gateway.ts index 676c13fa19..2a6f091e4e 100644 --- a/src/server/types/gateway.ts +++ b/src/server/types/gateway.ts @@ -1,3 +1,5 @@ +import type {ApiServiceActionConfig} from '@gravity-ui/gateway'; + import type {anonymousSchema, authSchema, schema} from '../../shared/schema'; export type DatalensGatewaySchemas = { @@ -5,3 +7,5 @@ export type DatalensGatewaySchemas = { auth: typeof authSchema; anonymous: typeof anonymousSchema; }; + +export type AnyApiServiceActionConfig = ApiServiceActionConfig; From eafd00faa5b855a79b33e390a5f0027ba436f34e Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Tue, 9 Sep 2025 15:14:27 +0300 Subject: [PATCH 17/40] Fixes --- src/server/controllers/public-api.ts | 83 +++++++++++++++------------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/src/server/controllers/public-api.ts b/src/server/controllers/public-api.ts index 345b1ba350..8b395831f0 100644 --- a/src/server/controllers/public-api.ts +++ b/src/server/controllers/public-api.ts @@ -1,11 +1,11 @@ import type {Request, Response} from '@gravity-ui/expresskit'; import {AppError, REQUEST_ID_PARAM_NAME} from '@gravity-ui/nodekit'; import _ from 'lodash'; +import type z from 'zod/v4'; import {ZodError} from 'zod/v4'; import {getValidationSchema} from '../../shared/schema/gateway-utils'; import {registerActionToOpenApi} from '../components/public-api'; -import type {PublicApiRpcMap} from '../components/public-api/types'; import {PUBLIC_API_RPC_ERROR_CODE} from '../constants/public-api'; import {registry} from '../registry'; import type {AnyApiServiceActionConfig, DatalensGatewaySchemas} from '../types/gateway'; @@ -27,6 +27,20 @@ const handleError = ( }); }; +const validateRequestBody = async (schema: z.ZodType, data: unknown): Promise => { + try { + return await schema.parseAsync(data); + } catch (error) { + if (error instanceof ZodError) { + throw new AppError('Validation error', { + details: error.issues, + }); + } + + throw error; + } +}; + export const createPublicApiController = () => { const {gatewayApi} = registry.getGatewayApi(); const schemasByScope = registry.getGatewaySchemasByScope(); @@ -61,70 +75,65 @@ export const createPublicApiController = () => { }); return async function publicApiController(req: Request, res: Response) { - const boundeHandler = handleError.bind(null, req, res); + const boundHandler = handleError.bind(null, req, res); - if (!req.params.version || !req.params.action) { - return boundeHandler(400, 'Invalid params, version or action are empty'); - } + try { + const {version, action: actionName} = req.params; - const version = req.params.version as keyof PublicApiRpcMap; - if (!_.has(proxyMap, version)) { - return boundeHandler(404, 'Version not found'); - } + if (!version) { + return boundHandler(400, 'Invalid params, version is empty'); + } - const versionMap = proxyMap[version]; - const actionName = req.params.action as keyof typeof versionMap; - if (!_.has(proxyMap[version], req.params.action)) { - return boundeHandler(404, 'Action not found'); - } + if (!actionName) { + return boundHandler(400, 'Invalid params, action is empty'); + } + + const versionMap = proxyMap[version]; + if (!versionMap) { + return boundHandler(404, 'Version not found'); + } - try { const action = versionMap[actionName]; + if (!action) { + return boundHandler(404, 'Action not found'); + } + const {ctx} = req; const headers = Utils.pickRpcHeaders(req); - const args = req.body; const requestId = ctx.get(REQUEST_ID_PARAM_NAME) || ''; const gatewayAction = action.resolve(gatewayApi); const gatewayActionConfig = actionToConfigMap.get(gatewayAction); if (!gatewayActionConfig) { - return boundeHandler(404, 'Action not found'); + return boundHandler(500, 'Action not found'); } const validationSchema = getValidationSchema(gatewayActionConfig); if (!validationSchema) { - return boundeHandler(404, 'Validation schema not found'); + return boundHandler(500, 'Validation schema not found'); } const {paramsSchema} = validationSchema; - try { - const validatedArgs = await paramsSchema.parseAsync(args); - - const result = await gatewayAction({ - headers, - args: validatedArgs, - ctx, - requestId, - }); - - res.status(200).send(result.responseData); - } catch (error) { - if (error instanceof ZodError) { - return boundeHandler(400, 'Validation error', error.issues); - } else { - throw error; - } - } + const validatedArgs = await validateRequestBody(paramsSchema, req.body); + + const result = await gatewayAction({ + headers, + args: validatedArgs, + ctx, + requestId, + }); + + res.status(200).send(result.responseData); } catch (err) { const {error} = err as any; if (error) { res.status(typeof error.status === 'number' ? error.status : 500).send(error); } else { - return boundeHandler(500, 'Unknown error'); + return boundHandler(500, 'Unknown error'); } } }; From 2c623209c82ea95af121d47b867eb971286f38a2 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Tue, 9 Sep 2025 17:05:29 +0300 Subject: [PATCH 18/40] Fixes --- src/server/components/public-api/constants.ts | 106 +++++----- src/shared/schema/bi/actions/connections.ts | 2 +- src/shared/schema/bi/actions/datasets.ts | 193 ++++++------------ src/shared/schema/bi/schemas/datasets.ts | 47 +++++ src/shared/schema/bi/schemas/index.ts | 1 + src/shared/schema/bi/types/datasets.ts | 39 +--- src/shared/schema/mix/actions/dash.ts | 2 +- src/shared/schema/mix/actions/entries.ts | 2 +- src/shared/schema/mix/actions/wizard.ts | 2 +- .../__tests__/dash-api.schema.test.ts | 2 +- .../dash-api.schema.ts | 2 +- .../dataset-api.schema.ts | 3 +- .../wizard-chart-api.schema.ts | 0 13 files changed, 178 insertions(+), 223 deletions(-) create mode 100644 src/shared/schema/bi/schemas/datasets.ts create mode 100644 src/shared/schema/bi/schemas/index.ts rename src/shared/sdk/{zod-shemas => zod-schemas}/__tests__/dash-api.schema.test.ts (99%) rename src/shared/sdk/{zod-shemas => zod-schemas}/dash-api.schema.ts (99%) rename src/shared/sdk/{zod-shemas => zod-schemas}/dataset-api.schema.ts (98%) rename src/shared/sdk/{zod-shemas => zod-schemas}/wizard-chart-api.schema.ts (100%) diff --git a/src/server/components/public-api/constants.ts b/src/server/components/public-api/constants.ts index 81519f6fee..0b2afe7527 100644 --- a/src/server/components/public-api/constants.ts +++ b/src/server/components/public-api/constants.ts @@ -20,13 +20,13 @@ enum ApiTag { export const PUBLIC_API_PROXY_MAP = { v0: { // navigation - getNavigationList: { - resolve: (api) => api.mix.getNavigationList, - openApi: { - summary: 'Get navigation list', - tags: [ApiTag.Navigation], - }, - }, + // getNavigationList: { + // resolve: (api) => api.mix.getNavigationList, + // openApi: { + // summary: 'Get navigation list', + // tags: [ApiTag.Navigation], + // }, + // }, // getStructureItems: { // resolve: (api) => api.us.getStructureItems, // openApi: { @@ -49,58 +49,58 @@ export const PUBLIC_API_PROXY_MAP = { // }, // }, // connection - getConnection: { - resolve: (api) => api.bi.getConnection, - openApi: { - summary: 'Get connection', - tags: [ApiTag.Connection], - }, - }, - updateConnection: { - resolve: (api) => api.bi.updateConnection, - openApi: { - summary: 'Update connection', - tags: [ApiTag.Connection], - }, - }, - createConnection: { - resolve: (api) => api.bi.createConnection, - openApi: { - summary: 'Create connection', - tags: [ApiTag.Connection], - }, - }, - deleteConnection: { - resolve: (api) => api.bi.deleteConnnection, - openApi: { - summary: 'Delete connection', - tags: [ApiTag.Connection], - }, - }, + // getConnection: { + // resolve: (api) => api.bi.getConnection, + // openApi: { + // summary: 'Get connection', + // tags: [ApiTag.Connection], + // }, + // }, + // updateConnection: { + // resolve: (api) => api.bi.updateConnection, + // openApi: { + // summary: 'Update connection', + // tags: [ApiTag.Connection], + // }, + // }, + // createConnection: { + // resolve: (api) => api.bi.createConnection, + // openApi: { + // summary: 'Create connection', + // tags: [ApiTag.Connection], + // }, + // }, + // deleteConnection: { + // resolve: (api) => api.bi.deleteConnection, + // openApi: { + // summary: 'Delete connection', + // tags: [ApiTag.Connection], + // }, + // }, // dataset getDataset: { - resolve: (api) => api.bi.getDatasetApi, + resolve: (api) => api.bi.getDatasetByVersion, openApi: { summary: 'Get dataset', tags: [ApiTag.Dataset], }, }, updateDataset: { - resolve: (api) => api.bi.updateDatasetApi, + resolve: (api) => api.bi.updateDataset, openApi: { summary: 'Update dataset', tags: [ApiTag.Dataset], }, }, createDataset: { - resolve: (api) => api.bi.createDatasetApi, + resolve: (api) => api.bi.createDataset, openApi: { summary: 'Create dataset', tags: [ApiTag.Dataset], }, }, deleteDataset: { - resolve: (api) => api.bi.deleteDatasetApi, + resolve: (api) => api.bi.deleteDataset, openApi: { summary: 'Delete dataset', tags: [ApiTag.Dataset], @@ -143,20 +143,20 @@ export const PUBLIC_API_PROXY_MAP = { tags: [ApiTag.Editor], }, }, - updateEditorChart: { - resolve: (api) => api.mix.updateEditorChart, - openApi: { - summary: 'Update editor chart', - tags: [ApiTag.Editor], - }, - }, - createEditorChart: { - resolve: (api) => api.mix.createEditorChart, - openApi: { - summary: 'Create editor chart', - tags: [ApiTag.Editor], - }, - }, + // updateEditorChart: { + // resolve: (api) => api.mix.updateEditorChart, + // openApi: { + // summary: 'Update editor chart', + // tags: [ApiTag.Editor], + // }, + // }, + // createEditorChart: { + // resolve: (api) => api.mix.createEditorChart, + // openApi: { + // summary: 'Create editor chart', + // tags: [ApiTag.Editor], + // }, + // }, deleteEditorChart: { resolve: (api) => api.mix.deleteEditorChartApi, openApi: { diff --git a/src/shared/schema/bi/actions/connections.ts b/src/shared/schema/bi/actions/connections.ts index 743a4e38bc..c0fcc896eb 100644 --- a/src/shared/schema/bi/actions/connections.ts +++ b/src/shared/schema/bi/actions/connections.ts @@ -95,7 +95,7 @@ export const actions = { params: ({connectionId: _connectionId, ...body}, headers) => ({body, headers}), transformResponseError: transformConnectionResponseError, }), - deleteConnnection: createAction({ + deleteConnection: createAction({ method: 'DELETE', path: ({connectionId}) => `${PATH_PREFIX}/connections/${filterUrlFragment(connectionId)}`, params: (_, headers) => ({headers}), diff --git a/src/shared/schema/bi/actions/datasets.ts b/src/shared/schema/bi/actions/datasets.ts index c2af6af774..011d1d9671 100644 --- a/src/shared/schema/bi/actions/datasets.ts +++ b/src/shared/schema/bi/actions/datasets.ts @@ -1,16 +1,10 @@ -import z from 'zod/v4'; - import { TIMEOUT_60_SEC, TIMEOUT_95_SEC, US_MASTER_TOKEN_HEADER, WORKBOOK_ID_HEADER, } from '../../../constants'; -import { - datasetBodySchema, - datasetOptionsSchema, - datasetSchema, -} from '../../../sdk/zod-shemas/dataset-api.schema'; +import {datasetSchema} from '../../../sdk/zod-schemas/dataset-api.schema'; import {createAction, createTypedAction} from '../../gateway-utils'; import {filterUrlFragment} from '../../utils'; import { @@ -19,6 +13,15 @@ import { transformValidateDatasetFormulaResponseError, transformValidateDatasetResponseError, } from '../helpers'; +import { + createDatasetArgsSchema, + createDatasetResultSchema, + deleteDatasetArgsSchema, + deleteDatasetResultSchema, + getDatasetByVersionArgsSchema, + updateDatasetArgsSchema, + updateDatasetResultSchema, +} from '../schemas'; import type { CheckConnectionsForPublicationArgs, CheckConnectionsForPublicationResponse, @@ -26,16 +29,10 @@ import type { CheckDatasetsForPublicationResponse, CopyDatasetArgs, CopyDatasetResponse, - CreateDatasetArgs, - CreateDatasetResponse, - DeleteDatasetArgs, - DeleteDatasetResponse, ExportDatasetArgs, ExportDatasetResponse, GetDataSetFieldsByIdArgs, GetDataSetFieldsByIdResponse, - GetDatasetByVersionArgs, - GetDatasetByVersionResponse, GetDistinctsApiV2Args, GetDistinctsApiV2Response, GetDistinctsApiV2TransformedResponse, @@ -46,8 +43,6 @@ import type { GetSourceResponse, ImportDatasetArgs, ImportDatasetResponse, - UpdateDatasetArgs, - UpdateDatasetResponse, ValidateDatasetArgs, ValidateDatasetFormulaArgs, ValidateDatasetFormulaResponse, @@ -58,18 +53,6 @@ const API_V1 = '/api/v1'; const API_DATA_V1 = '/api/data/v1'; const API_DATA_V2 = '/api/data/v2'; -const createDatasetDefaultArgsSchema = z.object({ - name: z.string(), - created_via: z.string().optional(), - multisource: z.boolean(), - dataset: datasetBodySchema, -}); - -const createDatasetArgsSchema = z.union([ - z.object({...createDatasetDefaultArgsSchema.shape, dir_path: z.string()}), - z.object({...createDatasetDefaultArgsSchema.shape, workbook_id: z.string()}), -]); - export const actions = { getSources: createAction({ method: 'GET', @@ -81,17 +64,25 @@ export const actions = { }), timeout: TIMEOUT_60_SEC, }), - getDatasetByVersion: createAction({ - method: 'GET', - path: ({datasetId, version}) => - `${API_V1}/datasets/${filterUrlFragment(datasetId)}/versions/${filterUrlFragment( - version, - )}`, - params: ({workbookId, rev_id}, headers) => ({ - headers: {...(workbookId ? {[WORKBOOK_ID_HEADER]: workbookId} : {}), ...headers}, - query: {rev_id}, - }), - }), + + getDatasetByVersion: createTypedAction( + { + paramsSchema: getDatasetByVersionArgsSchema, + resultSchema: datasetSchema, + }, + { + method: 'GET', + path: ({datasetId, version}) => + `${API_V1}/datasets/${filterUrlFragment(datasetId)}/versions/${filterUrlFragment( + version, + )}`, + params: ({workbookId, rev_id}, headers) => ({ + headers: {...(workbookId ? {[WORKBOOK_ID_HEADER]: workbookId} : {}), ...headers}, + query: {rev_id}, + }), + }, + ), + getFieldTypes: createAction({ method: 'GET', path: () => `${API_V1}/info/field_types`, @@ -151,14 +142,20 @@ export const actions = { headers: {...(workbookId ? {[WORKBOOK_ID_HEADER]: workbookId} : {}), ...headers}, }), }), - createDataset: createAction({ - method: 'POST', - path: () => `${API_V1}/datasets`, - params: ({dataset, ...restBody}, headers, {ctx}) => { - const resultDataset = prepareDatasetProperty(ctx, dataset); - return {body: {...restBody, dataset: resultDataset}, headers}; + createDataset: createTypedAction( + { + paramsSchema: createDatasetArgsSchema, + resultSchema: createDatasetResultSchema, }, - }), + { + method: 'POST', + path: () => `${API_V1}/datasets`, + params: ({dataset, ...restBody}, headers, {ctx}) => { + const resultDataset = prepareDatasetProperty(ctx, dataset); + return {body: {...restBody, dataset: resultDataset}, headers}; + }, + }, + ), validateDataset: createAction({ method: 'POST', path: ({datasetId, version}) => @@ -177,17 +174,24 @@ export const actions = { transformResponseError: transformValidateDatasetResponseError, timeout: TIMEOUT_95_SEC, }), - updateDataset: createAction({ - method: 'PUT', - path: ({datasetId, version}) => - `${API_V1}/datasets/${filterUrlFragment(datasetId)}/versions/${filterUrlFragment( - version, - )}`, - params: ({dataset, multisource}, headers, {ctx}) => { - const resultDataset = prepareDatasetProperty(ctx, dataset); - return {body: {dataset: resultDataset, multisource}, headers}; + + updateDataset: createTypedAction( + { + paramsSchema: updateDatasetArgsSchema, + resultSchema: updateDatasetResultSchema, }, - }), + { + method: 'PUT', + path: ({datasetId, version}) => + `${API_V1}/datasets/${filterUrlFragment(datasetId)}/versions/${filterUrlFragment( + version, + )}`, + params: ({dataset, multisource}, headers, {ctx}) => { + const resultDataset = prepareDatasetProperty(ctx, dataset); + return {body: {dataset: resultDataset, multisource}, headers}; + }, + }, + ), getPreview: createAction({ method: 'POST', endpoint: 'datasetDataApiEndpoint', @@ -266,61 +270,11 @@ export const actions = { transformResponseData: transformApiV2DistinctsResponse, timeout: TIMEOUT_95_SEC, }), - deleteDataset: createAction({ - method: 'DELETE', - path: ({datasetId}) => `${API_V1}/datasets/${filterUrlFragment(datasetId)}`, - params: (_, headers) => ({headers}), - }), - createDatasetApi: createTypedAction( - { - paramsSchema: createDatasetArgsSchema, - resultSchema: z.object({ - id: z.string(), - dataset: datasetBodySchema, - options: datasetOptionsSchema, - }), - }, - { - method: 'POST', - path: () => `${API_V1}/datasets`, - params: ({dataset, ...restBody}, headers, {ctx}) => { - const resultDataset = prepareDatasetProperty(ctx, dataset); - return {body: {...restBody, dataset: resultDataset}, headers}; - }, - }, - ), - updateDatasetApi: createTypedAction( - { - paramsSchema: z.object({ - version: z.literal('draft'), - datasetId: z.string(), - multisource: z.boolean(), - dataset: datasetBodySchema, - }), - resultSchema: z.object({ - id: z.string(), - dataset: datasetBodySchema, - options: datasetOptionsSchema, - }), - }, - { - method: 'PUT', - path: ({datasetId, version}) => - `${API_V1}/datasets/${filterUrlFragment(datasetId)}/versions/${filterUrlFragment( - version, - )}`, - params: ({dataset, multisource}, headers, {ctx}) => { - const resultDataset = prepareDatasetProperty(ctx, dataset); - return {body: {dataset: resultDataset, multisource}, headers}; - }, - }, - ), - deleteDatasetApi: createTypedAction( + + deleteDataset: createTypedAction( { - paramsSchema: z.object({ - datasetId: z.string(), - }), - resultSchema: z.unknown(), + paramsSchema: deleteDatasetArgsSchema, + resultSchema: deleteDatasetResultSchema, }, { method: 'DELETE', @@ -328,26 +282,7 @@ export const actions = { params: (_, headers) => ({headers}), }, ), - getDatasetApi: createTypedAction( - { - paramsSchema: z.object({ - datasetId: z.string(), - version: z.literal('draft'), - workbookId: z.union([z.null(), z.string()]), - }), - resultSchema: datasetSchema, - }, - { - method: 'GET', - path: ({datasetId, version}) => - `${API_V1}/datasets/${filterUrlFragment(datasetId)}/versions/${filterUrlFragment( - version, - )}`, - params: ({workbookId}, headers) => ({ - headers: {...(workbookId ? {[WORKBOOK_ID_HEADER]: workbookId} : {}), ...headers}, - }), - }, - ), + _proxyExportDataset: createAction({ method: 'POST', path: ({datasetId}) => `${API_V1}/datasets/export/${datasetId}`, diff --git a/src/shared/schema/bi/schemas/datasets.ts b/src/shared/schema/bi/schemas/datasets.ts new file mode 100644 index 0000000000..cb8abddf1f --- /dev/null +++ b/src/shared/schema/bi/schemas/datasets.ts @@ -0,0 +1,47 @@ +import z from 'zod/v4'; + +import {datasetBodySchema, datasetOptionsSchema} from '../../../sdk/zod-schemas/dataset-api.schema'; + +const createDatasetDefaultArgsSchema = z.object({ + name: z.string(), + created_via: z.string().optional(), + multisource: z.boolean(), + dataset: datasetBodySchema, +}); + +export const createDatasetArgsSchema = z.union([ + z.object({...createDatasetDefaultArgsSchema.shape, dir_path: z.string()}), + z.object({...createDatasetDefaultArgsSchema.shape, workbook_id: z.string()}), +]); + +export const createDatasetResultSchema = z.object({ + id: z.string(), + dataset: datasetBodySchema, + options: datasetOptionsSchema, +}); + +export const updateDatasetArgsSchema = z.object({ + version: z.literal('draft'), + datasetId: z.string(), + multisource: z.boolean(), + dataset: datasetBodySchema, +}); + +export const updateDatasetResultSchema = z.object({ + id: z.string(), + dataset: datasetBodySchema, + options: datasetOptionsSchema, +}); + +export const deleteDatasetArgsSchema = z.object({ + datasetId: z.string(), +}); + +export const deleteDatasetResultSchema = z.unknown(); + +export const getDatasetByVersionArgsSchema = z.object({ + datasetId: z.string(), + version: z.literal('draft'), + workbookId: z.union([z.null(), z.string()]), + rev_id: z.string().optional(), +}); diff --git a/src/shared/schema/bi/schemas/index.ts b/src/shared/schema/bi/schemas/index.ts new file mode 100644 index 0000000000..81df9143bc --- /dev/null +++ b/src/shared/schema/bi/schemas/index.ts @@ -0,0 +1 @@ +export * from './datasets'; diff --git a/src/shared/schema/bi/types/datasets.ts b/src/shared/schema/bi/types/datasets.ts index 13187e8038..8287a5ebb6 100644 --- a/src/shared/schema/bi/types/datasets.ts +++ b/src/shared/schema/bi/types/datasets.ts @@ -1,3 +1,5 @@ +import type z from 'zod/v4'; + import type { Dataset, DatasetField, @@ -8,6 +10,7 @@ import type { } from '../../../types'; import type {ApiV2RequestBody, ApiV2ResultData} from '../../../types/bi-api/v2'; import type {EntryFieldData} from '../../types'; +import type {createDatasetResultSchema, deleteDatasetResultSchema} from '../schemas'; import type {WorkbookIdArg} from './common'; @@ -63,14 +66,7 @@ export type GetSourceArgs = { limit?: number; } & WorkbookIdArg; -export type DeleteDatasetResponse = unknown; - -export type DeleteDatasetArgs = DatasetId; - -export type GetDatasetByVersionResponse = Dataset; - -export type GetDatasetByVersionArgs = {version: string; rev_id?: string} & DatasetId & - WorkbookIdArg; +export type DeleteDatasetResponse = z.infer; export type CheckDatasetsForPublicationResponse = { result: { @@ -140,32 +136,7 @@ export type GetDataSetFieldsByIdArgs = WorkbookIdArg & { dataSetId: string; }; -export type CreateDatasetResponse = Id & DatasetWithOptions; - -type CreateDatasetBaseArgs = { - dataset: Dataset['dataset']; - multisource: boolean; - name: string; - created_via?: string; -}; - -type CreateDirDatasetArgs = CreateDatasetBaseArgs & { - dir_path: string; -}; - -type CreateWorkbookDatsetArgs = CreateDatasetBaseArgs & { - workbook_id: string; -}; - -export type CreateDatasetArgs = CreateDirDatasetArgs | CreateWorkbookDatsetArgs; - -export type UpdateDatasetResponse = DatasetWithOptions; - -export type UpdateDatasetArgs = { - dataset: Dataset['dataset']; - version: DatasetVersion; - multisource: boolean; -} & DatasetId; +export type CreateDatasetResponse = z.infer; export type GetPreviewResponse = Partial; diff --git a/src/shared/schema/mix/actions/dash.ts b/src/shared/schema/mix/actions/dash.ts index 84f31d6d7b..ec9ac81932 100644 --- a/src/shared/schema/mix/actions/dash.ts +++ b/src/shared/schema/mix/actions/dash.ts @@ -4,7 +4,7 @@ import {z} from 'zod/v4'; import Dash from '../../../../server/components/sdk/dash'; import {DASH_ENTRY_RELEVANT_FIELDS} from '../../../../server/constants'; -import {dashSchema} from '../../../sdk/zod-shemas/dash-api.schema'; +import {dashSchema} from '../../../sdk/zod-schemas/dash-api.schema'; import type {ChartsStats} from '../../../types/charts'; import {EntryScope} from '../../../types/common'; import {createAction, createTypedAction} from '../../gateway-utils'; diff --git a/src/shared/schema/mix/actions/entries.ts b/src/shared/schema/mix/actions/entries.ts index 140702e136..489551c3e3 100644 --- a/src/shared/schema/mix/actions/entries.ts +++ b/src/shared/schema/mix/actions/entries.ts @@ -40,7 +40,7 @@ export const entriesActions = { return data; } case EntryScope.Connection: { - const data = await typedApi.bi.deleteConnnection({connectionId: entryId}); + const data = await typedApi.bi.deleteConnection({connectionId: entryId}); return data; } default: { diff --git a/src/shared/schema/mix/actions/wizard.ts b/src/shared/schema/mix/actions/wizard.ts index 075c9307a9..1f45fc7189 100644 --- a/src/shared/schema/mix/actions/wizard.ts +++ b/src/shared/schema/mix/actions/wizard.ts @@ -1,7 +1,7 @@ import z from 'zod/v4'; import {USProvider} from '../../../../server/components/charts-engine/components/storage/united-storage/provider'; -import {v12ChartsConfigSchema} from '../../../sdk/zod-shemas/wizard-chart-api.schema'; +import {v12ChartsConfigSchema} from '../../../sdk/zod-schemas/wizard-chart-api.schema'; import {EntryScope, WizardType} from '../../../types'; import {createTypedAction} from '../../gateway-utils'; import {getTypedApi} from '../../simple-schema'; diff --git a/src/shared/sdk/zod-shemas/__tests__/dash-api.schema.test.ts b/src/shared/sdk/zod-schemas/__tests__/dash-api.schema.test.ts similarity index 99% rename from src/shared/sdk/zod-shemas/__tests__/dash-api.schema.test.ts rename to src/shared/sdk/zod-schemas/__tests__/dash-api.schema.test.ts index 4748c10a78..fccb1451a3 100644 --- a/src/shared/sdk/zod-shemas/__tests__/dash-api.schema.test.ts +++ b/src/shared/sdk/zod-schemas/__tests__/dash-api.schema.test.ts @@ -7,7 +7,7 @@ import { DashTabItemControlSourceType, DashTabItemTitleSizes, DashTabItemType, -} from '../../../'; +} from '../../..'; import {type DashSchema, dashSchema} from '../dash-api.schema'; const DASH_DEFAULT_NAMESPACE = 'default'; diff --git a/src/shared/sdk/zod-shemas/dash-api.schema.ts b/src/shared/sdk/zod-schemas/dash-api.schema.ts similarity index 99% rename from src/shared/sdk/zod-shemas/dash-api.schema.ts rename to src/shared/sdk/zod-schemas/dash-api.schema.ts index d1393a5275..f1918d1325 100644 --- a/src/shared/sdk/zod-shemas/dash-api.schema.ts +++ b/src/shared/sdk/zod-schemas/dash-api.schema.ts @@ -9,7 +9,7 @@ import { DashTabItemControlSourceType, DashTabItemTitleSizes, DashTabItemType, -} from '../../'; +} from '../..'; const DASH_DEFAULT_NAMESPACE = 'default'; // Text definition diff --git a/src/shared/sdk/zod-shemas/dataset-api.schema.ts b/src/shared/sdk/zod-schemas/dataset-api.schema.ts similarity index 98% rename from src/shared/sdk/zod-shemas/dataset-api.schema.ts rename to src/shared/sdk/zod-schemas/dataset-api.schema.ts index 79926e1026..fcde63d51d 100644 --- a/src/shared/sdk/zod-shemas/dataset-api.schema.ts +++ b/src/shared/sdk/zod-schemas/dataset-api.schema.ts @@ -1,5 +1,6 @@ import * as z from 'zod/v4'; +import type {ConnectorType} from '../..'; import { DATASET_FIELD_TYPES, DATASET_VALUE_CONSTRAINT_TYPE, @@ -215,7 +216,7 @@ const datasetOptionsSchema = z.object({ id: z.string(), replacement_types: z.array( z.object({ - conn_type: z.string(), // ConnectorType but using string for flexibility + conn_type: z.string() as z.ZodSchema, // ConnectorType but using string for flexibility }), ), }), diff --git a/src/shared/sdk/zod-shemas/wizard-chart-api.schema.ts b/src/shared/sdk/zod-schemas/wizard-chart-api.schema.ts similarity index 100% rename from src/shared/sdk/zod-shemas/wizard-chart-api.schema.ts rename to src/shared/sdk/zod-schemas/wizard-chart-api.schema.ts From f05fe9a885e40b1599d6f753c25c78bb65e4c880 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Wed, 10 Sep 2025 09:51:06 +0300 Subject: [PATCH 19/40] Fixes --- src/shared/sdk/zod-schemas/dataset-api.schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/sdk/zod-schemas/dataset-api.schema.ts b/src/shared/sdk/zod-schemas/dataset-api.schema.ts index fcde63d51d..8623d4b787 100644 --- a/src/shared/sdk/zod-schemas/dataset-api.schema.ts +++ b/src/shared/sdk/zod-schemas/dataset-api.schema.ts @@ -1,6 +1,6 @@ import * as z from 'zod/v4'; -import type {ConnectorType} from '../..'; +import {ConnectorType} from '../..'; import { DATASET_FIELD_TYPES, DATASET_VALUE_CONSTRAINT_TYPE, @@ -216,7 +216,7 @@ const datasetOptionsSchema = z.object({ id: z.string(), replacement_types: z.array( z.object({ - conn_type: z.string() as z.ZodSchema, // ConnectorType but using string for flexibility + conn_type: z.enum(ConnectorType), }), ), }), From c1926b09a55533331f767646d97ee424f350662b Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Wed, 10 Sep 2025 10:20:00 +0300 Subject: [PATCH 20/40] Fixes --- src/shared/schema/gateway-utils.ts | 15 ++++----------- src/shared/schema/mix/actions/navigation.ts | 1 + 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/shared/schema/gateway-utils.ts b/src/shared/schema/gateway-utils.ts index 814f7f8f0d..46d7b47314 100644 --- a/src/shared/schema/gateway-utils.ts +++ b/src/shared/schema/gateway-utils.ts @@ -44,22 +44,15 @@ export const getValidationSchema = (value: object): TypedActionSchema | null => return hasValidationSchema(value) ? value[VALIDATION_SCHEMA_KEY] : null; }; -export function createTypedAction< - TOutputSchema extends z.ZodType, - TParamsSchema extends z.ZodType, - TTransformedSchema extends z.ZodType = TOutputSchema, - TOutput = z.infer, - TParams = z.infer, - TTransformed = z.infer, ->( +export function createTypedAction( schema: {paramsSchema: TParamsSchema; resultSchema: TOutputSchema}, actionConfig: ApiServiceActionConfig< AppContext, Request, Response, - TOutput, - TParams, - TTransformed + z.infer, + z.infer, + z.infer >, ) { const schemaValidationObject = { diff --git a/src/shared/schema/mix/actions/navigation.ts b/src/shared/schema/mix/actions/navigation.ts index 3ce49ca5fc..efba1faba4 100644 --- a/src/shared/schema/mix/actions/navigation.ts +++ b/src/shared/schema/mix/actions/navigation.ts @@ -34,6 +34,7 @@ export const navigationActions = { excludeLocked: true, }); } + return { breadCrumbs: 'breadCrumbs' in data ? data.breadCrumbs : [], hasNextPage: data.hasNextPage, From ef227f107a3d7bf29eae4ef3b79d352e44dd7609 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Wed, 10 Sep 2025 11:02:11 +0300 Subject: [PATCH 21/40] Fixes --- src/server/components/public-api/constants.ts | 20 +++++++++---------- src/shared/schema/mix/actions/dash.ts | 12 +++++++---- src/shared/schema/mix/actions/editor.ts | 6 ++++-- src/shared/schema/mix/actions/wizard.ts | 12 +++++++---- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/server/components/public-api/constants.ts b/src/server/components/public-api/constants.ts index 0b2afe7527..e725011c07 100644 --- a/src/server/components/public-api/constants.ts +++ b/src/server/components/public-api/constants.ts @@ -108,28 +108,28 @@ export const PUBLIC_API_PROXY_MAP = { }, // wizard getWizardChart: { - resolve: (api) => api.mix.getWizardChartApi, + resolve: (api) => api.mix.getWizardChart, openApi: { summary: 'Get wizard chart', tags: [ApiTag.Wizard], }, }, updateWizardChart: { - resolve: (api) => api.mix.updateWizardChartApi, + resolve: (api) => api.mix.updateWizardChart, openApi: { summary: 'Update wizard chart', tags: [ApiTag.Wizard], }, }, createWizardChart: { - resolve: (api) => api.mix.createWizardChartApi, + resolve: (api) => api.mix.createWizardChart, openApi: { summary: 'Create wizard chart', tags: [ApiTag.Wizard], }, }, deleteWizardChart: { - resolve: (api) => api.mix.deleteWizardChartApi, + resolve: (api) => api.mix.deleteWizardChart, openApi: { summary: 'Delete wizard chart', tags: [ApiTag.Wizard], @@ -137,7 +137,7 @@ export const PUBLIC_API_PROXY_MAP = { }, // editor getEditorChart: { - resolve: (api) => api.mix.getEditorChartApi, + resolve: (api) => api.mix.getEditorChart, openApi: { summary: 'Get editor chart', tags: [ApiTag.Editor], @@ -158,7 +158,7 @@ export const PUBLIC_API_PROXY_MAP = { // }, // }, deleteEditorChart: { - resolve: (api) => api.mix.deleteEditorChartApi, + resolve: (api) => api.mix.deleteEditorChart, openApi: { summary: 'Delete editor chart', tags: [ApiTag.Editor], @@ -166,28 +166,28 @@ export const PUBLIC_API_PROXY_MAP = { }, // Dash getDashboard: { - resolve: (api) => api.mix.getDashboardApi, + resolve: (api) => api.mix.getDashboard, openApi: { summary: 'Get dashboard', tags: [ApiTag.Dashboard], }, }, updateDashboard: { - resolve: (api) => api.mix.updateDashboardApi, + resolve: (api) => api.mix.updateDashboard, openApi: { summary: 'Delete dashboard', tags: [ApiTag.Dashboard], }, }, createDashboard: { - resolve: (api) => api.mix.createDashboardApi, + resolve: (api) => api.mix.createDashboard, openApi: { summary: 'Create dashboard', tags: [ApiTag.Dashboard], }, }, deleteDashboard: { - resolve: (api) => api.mix.deleteDashboardApi, + resolve: (api) => api.mix.deleteDashboard, openApi: { summary: 'Delete dashboard', tags: [ApiTag.Dashboard], diff --git a/src/shared/schema/mix/actions/dash.ts b/src/shared/schema/mix/actions/dash.ts index ec9ac81932..8113601f4c 100644 --- a/src/shared/schema/mix/actions/dash.ts +++ b/src/shared/schema/mix/actions/dash.ts @@ -61,7 +61,8 @@ const dashUsUpdateSchema = z.object({ }); export const dashActions = { - getDashboardApi: createTypedAction( + // WIP + getDashboard: createTypedAction( { paramsSchema: z.object({ dashboardId: z.string(), @@ -99,7 +100,8 @@ export const dashActions = { return pick(result, DASH_ENTRY_RELEVANT_FIELDS) as any; }, ), - deleteDashboardApi: createTypedAction( + // WIP + deleteDashboard: createTypedAction( { paramsSchema: z.object({ dashboardId: z.string(), @@ -116,7 +118,8 @@ export const dashActions = { }); }, ), - updateDashboardApi: createTypedAction( + // WIP + updateDashboard: createTypedAction( { paramsSchema: dashUsUpdateSchema, resultSchema: dashUsSchema, @@ -131,7 +134,8 @@ export const dashActions = { })) as unknown as z.infer; }, ), - createDashboardApi: createTypedAction( + // WIP + createDashboard: createTypedAction( { paramsSchema: dashUsCreateSchema, resultSchema: dashUsSchema, diff --git a/src/shared/schema/mix/actions/editor.ts b/src/shared/schema/mix/actions/editor.ts index 6f14aaa3e0..104f28c478 100644 --- a/src/shared/schema/mix/actions/editor.ts +++ b/src/shared/schema/mix/actions/editor.ts @@ -48,7 +48,8 @@ const editorUsSchema = z.object({ }); export const editorActions = { - getEditorChartApi: createTypedAction( + // WIP + getEditorChart: createTypedAction( { paramsSchema: z.object({ chardId: z.string(), @@ -108,7 +109,8 @@ export const editorActions = { } }, ), - deleteEditorChartApi: createTypedAction( + // WIP + deleteEditorChart: createTypedAction( { paramsSchema: z.object({ chartId: z.string(), diff --git a/src/shared/schema/mix/actions/wizard.ts b/src/shared/schema/mix/actions/wizard.ts index 1f45fc7189..be9fb05e69 100644 --- a/src/shared/schema/mix/actions/wizard.ts +++ b/src/shared/schema/mix/actions/wizard.ts @@ -27,7 +27,8 @@ const wizardUsSchema = z.object({ }); export const wizardActions = { - getWizardChartApi: createTypedAction( + // WIP + getWizardChart: createTypedAction( { paramsSchema: z.object({ chardId: z.string(), @@ -53,7 +54,8 @@ export const wizardActions = { return result as any; }, ), - createWizardChartApi: createTypedAction( + // WIP + createWizardChart: createTypedAction( { paramsSchema: z.object({ entryId: z.string(), @@ -81,7 +83,8 @@ export const wizardActions = { return result as any; }, ), - updateWizardChartApi: createTypedAction( + // WIP + updateWizardChart: createTypedAction( { paramsSchema: z.object({ entryId: z.string(), @@ -105,7 +108,8 @@ export const wizardActions = { return result as any; }, ), - deleteWizardChartApi: createTypedAction( + // WIP + deleteWizardChart: createTypedAction( { paramsSchema: z.object({ chartId: z.string(), From fc03943df194bec5d4fc8a374ecd31d355a63d98 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Wed, 10 Sep 2025 16:24:40 +0300 Subject: [PATCH 22/40] Fixes --- src/server/controllers/public-api.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/server/controllers/public-api.ts b/src/server/controllers/public-api.ts index 8b395831f0..7b97dc7081 100644 --- a/src/server/controllers/public-api.ts +++ b/src/server/controllers/public-api.ts @@ -10,6 +10,7 @@ import {PUBLIC_API_RPC_ERROR_CODE} from '../constants/public-api'; import {registry} from '../registry'; import type {AnyApiServiceActionConfig, DatalensGatewaySchemas} from '../types/gateway'; import Utils from '../utils'; +import {isGatewayError} from '../utils/gateway'; const handleError = ( req: Request, @@ -107,13 +108,15 @@ export const createPublicApiController = () => { const gatewayActionConfig = actionToConfigMap.get(gatewayAction); if (!gatewayActionConfig) { - return boundHandler(500, 'Action not found'); + req.ctx.logError(`Couldn't find action config in actionToConfigMap`); + return boundHandler(500, 'Unknown error'); } const validationSchema = getValidationSchema(gatewayActionConfig); if (!validationSchema) { - return boundHandler(500, 'Validation schema not found'); + req.ctx.logError(`Couldn't find action validation schema`); + return boundHandler(500, 'Unknown error'); } const {paramsSchema} = validationSchema; @@ -128,10 +131,9 @@ export const createPublicApiController = () => { }); res.status(200).send(result.responseData); - } catch (err) { - const {error} = err as any; - if (error) { - res.status(typeof error.status === 'number' ? error.status : 500).send(error); + } catch (err: unknown) { + if (isGatewayError(err)) { + return boundHandler(500, 'Gateway error'); } else { return boundHandler(500, 'Unknown error'); } From b71dffb712884bbb9834b8ed9e6ba1e24b6db7f9 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Wed, 10 Sep 2025 16:43:53 +0300 Subject: [PATCH 23/40] Fixes --- src/shared/components/api-docs/index.ts | 0 src/shared/schema/gateway-utils.ts | 7 +----- src/shared/sdk/BaseSdk.ts | 32 ------------------------- src/shared/sdk/index.ts | 0 4 files changed, 1 insertion(+), 38 deletions(-) delete mode 100644 src/shared/components/api-docs/index.ts delete mode 100644 src/shared/sdk/BaseSdk.ts delete mode 100644 src/shared/sdk/index.ts diff --git a/src/shared/components/api-docs/index.ts b/src/shared/components/api-docs/index.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/shared/schema/gateway-utils.ts b/src/shared/schema/gateway-utils.ts index 46d7b47314..d7f96b4e81 100644 --- a/src/shared/schema/gateway-utils.ts +++ b/src/shared/schema/gateway-utils.ts @@ -1,10 +1,5 @@ import type {Request, Response} from '@gravity-ui/expresskit'; -import type { - ApiServiceActionConfig, - // ApiServiceMixedActionConfig, - // ApiServiceRestActionConfig, - GetAuthHeaders, -} from '@gravity-ui/gateway'; +import type {ApiServiceActionConfig, GetAuthHeaders} from '@gravity-ui/gateway'; import type {AppContext} from '@gravity-ui/nodekit'; import type z from 'zod/v4'; diff --git a/src/shared/sdk/BaseSdk.ts b/src/shared/sdk/BaseSdk.ts deleted file mode 100644 index 593ccf7c3f..0000000000 --- a/src/shared/sdk/BaseSdk.ts +++ /dev/null @@ -1,32 +0,0 @@ -export abstract class BaseSdk< - T, - M extends Record T> = {}, - S extends Record = {}, - O extends object = {}, -> { - abstract mutations?: M; - abstract selectors?: S; - - abstract create(): Promise; - abstract get(options: {id: string} & O): Promise; - abstract update(options: {id: string} & O, newEntry: T): Promise; - abstract delete(options: {id: string} & O): Promise; - - async lazyBatch(options: {id: string} & O) { - const entry = await this.get(options); - - return this.batchMutations.bind(this, entry); - } - - batchMutations(entry: T, mutations: Array<{name: keyof M; options: any}>) { - let newEntry = entry; - - for (const mutation of mutations) { - if (this.mutations && mutation.name in this.mutations) { - newEntry = this.mutations[mutation.name](entry, mutation.options); - } - } - - return newEntry; - } -} diff --git a/src/shared/sdk/index.ts b/src/shared/sdk/index.ts deleted file mode 100644 index e69de29bb2..0000000000 From d6bc6ce2d13a3d086f4352376a60ed39128d1127 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Thu, 11 Sep 2025 10:47:28 +0300 Subject: [PATCH 24/40] Fixes --- .../controllers/public-api/constants.ts | 10 ++ .../{public-api.ts => public-api/index.ts} | 89 ++++++------------ src/server/controllers/public-api/utils.ts | 93 +++++++++++++++++++ 3 files changed, 130 insertions(+), 62 deletions(-) create mode 100644 src/server/controllers/public-api/constants.ts rename src/server/controllers/{public-api.ts => public-api/index.ts} (58%) create mode 100644 src/server/controllers/public-api/utils.ts diff --git a/src/server/controllers/public-api/constants.ts b/src/server/controllers/public-api/constants.ts new file mode 100644 index 0000000000..5d91666b23 --- /dev/null +++ b/src/server/controllers/public-api/constants.ts @@ -0,0 +1,10 @@ +import {AppError} from '@gravity-ui/nodekit'; + +export class PublicApiError extends AppError {} + +export const PUBLIC_API_ERRORS = { + VALIDATION_ERROR: 'VALIDATION_ERROR', + ENDPOINT_NOT_FOUND: 'ENDPOINT_NOT_FOUND', + ACTION_CONFIG_NOT_FOUND: 'ACTION_CONFIG_NOT_FOUND', + ACTION_VALIDATION_SCHEMA_NOT_FOUND: 'ACTION_VALIDATION_SCHEMA_NOT_FOUND', +} as const; diff --git a/src/server/controllers/public-api.ts b/src/server/controllers/public-api/index.ts similarity index 58% rename from src/server/controllers/public-api.ts rename to src/server/controllers/public-api/index.ts index 7b97dc7081..af7967ce90 100644 --- a/src/server/controllers/public-api.ts +++ b/src/server/controllers/public-api/index.ts @@ -1,46 +1,15 @@ import type {Request, Response} from '@gravity-ui/expresskit'; import {AppError, REQUEST_ID_PARAM_NAME} from '@gravity-ui/nodekit'; import _ from 'lodash'; -import type z from 'zod/v4'; -import {ZodError} from 'zod/v4'; - -import {getValidationSchema} from '../../shared/schema/gateway-utils'; -import {registerActionToOpenApi} from '../components/public-api'; -import {PUBLIC_API_RPC_ERROR_CODE} from '../constants/public-api'; -import {registry} from '../registry'; -import type {AnyApiServiceActionConfig, DatalensGatewaySchemas} from '../types/gateway'; -import Utils from '../utils'; -import {isGatewayError} from '../utils/gateway'; - -const handleError = ( - req: Request, - res: Response, - status: number, - message: string, - details?: unknown, -) => { - res.status(status).send({ - status, - code: PUBLIC_API_RPC_ERROR_CODE, - message, - requestId: req.ctx.get(REQUEST_ID_PARAM_NAME) || '', - details, - }); -}; -const validateRequestBody = async (schema: z.ZodType, data: unknown): Promise => { - try { - return await schema.parseAsync(data); - } catch (error) { - if (error instanceof ZodError) { - throw new AppError('Validation error', { - details: error.issues, - }); - } +import {getValidationSchema} from '../../../shared/schema/gateway-utils'; +import {registerActionToOpenApi} from '../../components/public-api'; +import {registry} from '../../registry'; +import type {AnyApiServiceActionConfig, DatalensGatewaySchemas} from '../../types/gateway'; +import Utils from '../../utils'; - throw error; - } -}; +import {PUBLIC_API_ERRORS, PublicApiError} from './constants'; +import {prepareError, validateRequestBody} from './utils'; export const createPublicApiController = () => { const {gatewayApi} = registry.getGatewayApi(); @@ -76,28 +45,16 @@ export const createPublicApiController = () => { }); return async function publicApiController(req: Request, res: Response) { - const boundHandler = handleError.bind(null, req, res); - try { const {version, action: actionName} = req.params; - if (!version) { - return boundHandler(400, 'Invalid params, version is empty'); + if (!version || !actionName || !proxyMap[version] || !proxyMap[version][actionName]) { + throw new PublicApiError(`Endpoint ${req.path} does not exist`, { + code: PUBLIC_API_ERRORS.ENDPOINT_NOT_FOUND, + }); } - if (!actionName) { - return boundHandler(400, 'Invalid params, action is empty'); - } - - const versionMap = proxyMap[version]; - if (!versionMap) { - return boundHandler(404, 'Version not found'); - } - - const action = versionMap[actionName]; - if (!action) { - return boundHandler(404, 'Action not found'); - } + const action = proxyMap[version][actionName]; const {ctx} = req; @@ -109,14 +66,18 @@ export const createPublicApiController = () => { if (!gatewayActionConfig) { req.ctx.logError(`Couldn't find action config in actionToConfigMap`); - return boundHandler(500, 'Unknown error'); + throw new PublicApiError(PUBLIC_API_ERRORS.ACTION_CONFIG_NOT_FOUND, { + code: PUBLIC_API_ERRORS.ACTION_CONFIG_NOT_FOUND, + }); } const validationSchema = getValidationSchema(gatewayActionConfig); if (!validationSchema) { req.ctx.logError(`Couldn't find action validation schema`); - return boundHandler(500, 'Unknown error'); + throw new PublicApiError(PUBLIC_API_ERRORS.ACTION_VALIDATION_SCHEMA_NOT_FOUND, { + code: PUBLIC_API_ERRORS.ACTION_VALIDATION_SCHEMA_NOT_FOUND, + }); } const {paramsSchema} = validationSchema; @@ -132,11 +93,15 @@ export const createPublicApiController = () => { res.status(200).send(result.responseData); } catch (err: unknown) { - if (isGatewayError(err)) { - return boundHandler(500, 'Gateway error'); - } else { - return boundHandler(500, 'Unknown error'); - } + const {status, message, code, details} = prepareError(err); + + res.status(status).send({ + status, + code, + message, + requestId: req.ctx.get(REQUEST_ID_PARAM_NAME) || '', + details, + }); } }; }; diff --git a/src/server/controllers/public-api/utils.ts b/src/server/controllers/public-api/utils.ts new file mode 100644 index 0000000000..963b109ba7 --- /dev/null +++ b/src/server/controllers/public-api/utils.ts @@ -0,0 +1,93 @@ +import {AppError} from '@gravity-ui/nodekit'; +import {AxiosError} from 'axios'; +import _, {isObject} from 'lodash'; +import type z from 'zod/v4'; +import {ZodError} from 'zod/v4'; + +import {isGatewayError} from '../../utils/gateway'; + +import {PUBLIC_API_ERRORS, PublicApiError} from './constants'; + +export const prepareError = ( + error: unknown, +): {status: number; message: string; code?: string; details?: unknown} => { + if (error instanceof PublicApiError) { + const {code, message, details} = error; + + switch (code) { + case PUBLIC_API_ERRORS.VALIDATION_ERROR: { + return {status: 400, message, code, details}; + } + + case PUBLIC_API_ERRORS.ENDPOINT_NOT_FOUND: { + return {status: 404, message, code, details}; + } + + default: { + return { + status: 500, + message: 'Internal server error', + }; + } + } + } + + if (isGatewayError(error)) { + const {error: innerError} = error; + + if (innerError.status !== 500) { + return { + status: innerError.status, + code: innerError.code, + message: innerError.message, + details: innerError.details, + }; + } + + const originalError = innerError.debug.originalError; + + if (originalError instanceof AxiosError) { + const status = originalError.status ?? 500; + let message = originalError.message; + let code: string | undefined; + let details: unknown; + + const data = originalError.response?.data; + + if (isObject(data)) { + if ('message' in data && typeof data.message === 'string') { + message = data.message; + } + + if ('code' in data && typeof data.code === 'string') { + code = data.code; + } + + if ('details' in data) { + details = data.details; + } + } + + return {status, message, code, details}; + } + } + + return { + status: 500, + message: 'Internal server error', + }; +}; + +export const validateRequestBody = async (schema: z.ZodType, data: unknown): Promise => { + try { + return await schema.parseAsync(data); + } catch (error) { + if (error instanceof ZodError) { + throw new AppError('Validation error', { + details: error.issues, + }); + } + + throw error; + } +}; From ee321049a1a5a0f51e3733e343afb908a5114f17 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Thu, 11 Sep 2025 11:14:56 +0300 Subject: [PATCH 25/40] Fixes --- src/server/components/public-api/constants.ts | 20 +++++++++---------- src/server/controllers/public-api/utils.ts | 8 ++++++++ src/shared/schema/mix/actions/dash.ts | 8 ++++---- src/shared/schema/mix/actions/editor.ts | 4 ++-- src/shared/schema/mix/actions/wizard.ts | 8 ++++---- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/server/components/public-api/constants.ts b/src/server/components/public-api/constants.ts index e725011c07..114d0915ac 100644 --- a/src/server/components/public-api/constants.ts +++ b/src/server/components/public-api/constants.ts @@ -108,28 +108,28 @@ export const PUBLIC_API_PROXY_MAP = { }, // wizard getWizardChart: { - resolve: (api) => api.mix.getWizardChart, + resolve: (api) => api.mix.__getWizardChart__, openApi: { summary: 'Get wizard chart', tags: [ApiTag.Wizard], }, }, updateWizardChart: { - resolve: (api) => api.mix.updateWizardChart, + resolve: (api) => api.mix.__updateWizardChart__, openApi: { summary: 'Update wizard chart', tags: [ApiTag.Wizard], }, }, createWizardChart: { - resolve: (api) => api.mix.createWizardChart, + resolve: (api) => api.mix.__createWizardChart__, openApi: { summary: 'Create wizard chart', tags: [ApiTag.Wizard], }, }, deleteWizardChart: { - resolve: (api) => api.mix.deleteWizardChart, + resolve: (api) => api.mix.__deleteWizardChart__, openApi: { summary: 'Delete wizard chart', tags: [ApiTag.Wizard], @@ -137,7 +137,7 @@ export const PUBLIC_API_PROXY_MAP = { }, // editor getEditorChart: { - resolve: (api) => api.mix.getEditorChart, + resolve: (api) => api.mix.__getEditorChart__, openApi: { summary: 'Get editor chart', tags: [ApiTag.Editor], @@ -158,7 +158,7 @@ export const PUBLIC_API_PROXY_MAP = { // }, // }, deleteEditorChart: { - resolve: (api) => api.mix.deleteEditorChart, + resolve: (api) => api.mix.__deleteEditorChart__, openApi: { summary: 'Delete editor chart', tags: [ApiTag.Editor], @@ -166,28 +166,28 @@ export const PUBLIC_API_PROXY_MAP = { }, // Dash getDashboard: { - resolve: (api) => api.mix.getDashboard, + resolve: (api) => api.mix.__getDashboard__, openApi: { summary: 'Get dashboard', tags: [ApiTag.Dashboard], }, }, updateDashboard: { - resolve: (api) => api.mix.updateDashboard, + resolve: (api) => api.mix.__updateDashboard__, openApi: { summary: 'Delete dashboard', tags: [ApiTag.Dashboard], }, }, createDashboard: { - resolve: (api) => api.mix.createDashboard, + resolve: (api) => api.mix.__createDashboard__, openApi: { summary: 'Create dashboard', tags: [ApiTag.Dashboard], }, }, deleteDashboard: { - resolve: (api) => api.mix.deleteDashboard, + resolve: (api) => api.mix.__deleteDashboard__, openApi: { summary: 'Delete dashboard', tags: [ApiTag.Dashboard], diff --git a/src/server/controllers/public-api/utils.ts b/src/server/controllers/public-api/utils.ts index 963b109ba7..9adc3f3eac 100644 --- a/src/server/controllers/public-api/utils.ts +++ b/src/server/controllers/public-api/utils.ts @@ -70,6 +70,14 @@ export const prepareError = ( return {status, message, code, details}; } + + if (originalError instanceof AppError) { + const message = originalError.message; + const code = originalError.code ? String(originalError.code) : undefined; + const details = originalError.details; + + return {status: 500, message, code, details}; + } } return { diff --git a/src/shared/schema/mix/actions/dash.ts b/src/shared/schema/mix/actions/dash.ts index 8113601f4c..b5e5514364 100644 --- a/src/shared/schema/mix/actions/dash.ts +++ b/src/shared/schema/mix/actions/dash.ts @@ -62,7 +62,7 @@ const dashUsUpdateSchema = z.object({ export const dashActions = { // WIP - getDashboard: createTypedAction( + __getDashboard__: createTypedAction( { paramsSchema: z.object({ dashboardId: z.string(), @@ -101,7 +101,7 @@ export const dashActions = { }, ), // WIP - deleteDashboard: createTypedAction( + __deleteDashboard__: createTypedAction( { paramsSchema: z.object({ dashboardId: z.string(), @@ -119,7 +119,7 @@ export const dashActions = { }, ), // WIP - updateDashboard: createTypedAction( + __updateDashboard__: createTypedAction( { paramsSchema: dashUsUpdateSchema, resultSchema: dashUsSchema, @@ -135,7 +135,7 @@ export const dashActions = { }, ), // WIP - createDashboard: createTypedAction( + __createDashboard__: createTypedAction( { paramsSchema: dashUsCreateSchema, resultSchema: dashUsSchema, diff --git a/src/shared/schema/mix/actions/editor.ts b/src/shared/schema/mix/actions/editor.ts index 104f28c478..17fd471ac4 100644 --- a/src/shared/schema/mix/actions/editor.ts +++ b/src/shared/schema/mix/actions/editor.ts @@ -49,7 +49,7 @@ const editorUsSchema = z.object({ export const editorActions = { // WIP - getEditorChart: createTypedAction( + __getEditorChart__: createTypedAction( { paramsSchema: z.object({ chardId: z.string(), @@ -110,7 +110,7 @@ export const editorActions = { }, ), // WIP - deleteEditorChart: createTypedAction( + __deleteEditorChart__: createTypedAction( { paramsSchema: z.object({ chartId: z.string(), diff --git a/src/shared/schema/mix/actions/wizard.ts b/src/shared/schema/mix/actions/wizard.ts index be9fb05e69..439a6d5da6 100644 --- a/src/shared/schema/mix/actions/wizard.ts +++ b/src/shared/schema/mix/actions/wizard.ts @@ -28,7 +28,7 @@ const wizardUsSchema = z.object({ export const wizardActions = { // WIP - getWizardChart: createTypedAction( + __getWizardChart__: createTypedAction( { paramsSchema: z.object({ chardId: z.string(), @@ -55,7 +55,7 @@ export const wizardActions = { }, ), // WIP - createWizardChart: createTypedAction( + __createWizardChart__: createTypedAction( { paramsSchema: z.object({ entryId: z.string(), @@ -84,7 +84,7 @@ export const wizardActions = { }, ), // WIP - updateWizardChart: createTypedAction( + __updateWizardChart__: createTypedAction( { paramsSchema: z.object({ entryId: z.string(), @@ -109,7 +109,7 @@ export const wizardActions = { }, ), // WIP - deleteWizardChart: createTypedAction( + __deleteWizardChart__: createTypedAction( { paramsSchema: z.object({ chartId: z.string(), From 4c1a1b4accd878cd988b4216b3b9cce934856abe Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Thu, 11 Sep 2025 12:14:41 +0300 Subject: [PATCH 26/40] Fixes --- src/server/controllers/public-api/utils.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/server/controllers/public-api/utils.ts b/src/server/controllers/public-api/utils.ts index 9adc3f3eac..f2478e4074 100644 --- a/src/server/controllers/public-api/utils.ts +++ b/src/server/controllers/public-api/utils.ts @@ -78,6 +78,14 @@ export const prepareError = ( return {status: 500, message, code, details}; } + + if ( + !(originalError instanceof TypeError) && + !(originalError instanceof ReferenceError) && + !(originalError instanceof SyntaxError) + ) { + return {status: innerError.status, message: innerError.message}; + } } return { From c409620b85344f93f067d514015170698d4d48cf Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Thu, 11 Sep 2025 12:17:05 +0300 Subject: [PATCH 27/40] Fixes --- src/server/controllers/public-api/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/controllers/public-api/utils.ts b/src/server/controllers/public-api/utils.ts index f2478e4074..720e630f4d 100644 --- a/src/server/controllers/public-api/utils.ts +++ b/src/server/controllers/public-api/utils.ts @@ -76,7 +76,7 @@ export const prepareError = ( const code = originalError.code ? String(originalError.code) : undefined; const details = originalError.details; - return {status: 500, message, code, details}; + return {status: innerError.status, message, code, details}; } if ( From b2c46d8912369dfd1afb19b50fb6459b8468a699 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Thu, 11 Sep 2025 14:37:43 +0300 Subject: [PATCH 28/40] Fixes --- src/shared/schema/mix/actions/dash.ts | 4 +++- src/shared/schema/mix/actions/editor.ts | 4 +++- src/shared/schema/mix/actions/wizard.ts | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/shared/schema/mix/actions/dash.ts b/src/shared/schema/mix/actions/dash.ts index b5e5514364..40da631f84 100644 --- a/src/shared/schema/mix/actions/dash.ts +++ b/src/shared/schema/mix/actions/dash.ts @@ -107,7 +107,7 @@ export const dashActions = { dashboardId: z.string(), lockToken: z.string().optional(), }), - resultSchema: z.any(), + resultSchema: z.object({}), }, async (api, {lockToken, dashboardId}) => { const typedApi = getTypedApi(api); @@ -116,6 +116,8 @@ export const dashActions = { entryId: dashboardId, lockToken, }); + + return {}; }, ), // WIP diff --git a/src/shared/schema/mix/actions/editor.ts b/src/shared/schema/mix/actions/editor.ts index 17fd471ac4..c312fe21b2 100644 --- a/src/shared/schema/mix/actions/editor.ts +++ b/src/shared/schema/mix/actions/editor.ts @@ -115,7 +115,7 @@ export const editorActions = { paramsSchema: z.object({ chartId: z.string(), }), - resultSchema: z.any(), + resultSchema: z.object({}), }, async (api, {chartId}) => { const typedApi = getTypedApi(api); @@ -123,6 +123,8 @@ export const editorActions = { await typedApi.us._deleteUSEntry({ entryId: chartId, }); + + return {}; }, ), }; diff --git a/src/shared/schema/mix/actions/wizard.ts b/src/shared/schema/mix/actions/wizard.ts index 439a6d5da6..42bec849bd 100644 --- a/src/shared/schema/mix/actions/wizard.ts +++ b/src/shared/schema/mix/actions/wizard.ts @@ -114,7 +114,7 @@ export const wizardActions = { paramsSchema: z.object({ chartId: z.string(), }), - resultSchema: z.any(), + resultSchema: z.object({}), }, async (api, {chartId}) => { const typedApi = getTypedApi(api); @@ -122,6 +122,8 @@ export const wizardActions = { await typedApi.us._deleteUSEntry({ entryId: chartId, }); + + return {}; }, ), }; From 1f6a310df5f11762c60e692248cc9916e68f675c Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Thu, 11 Sep 2025 16:47:37 +0300 Subject: [PATCH 29/40] Fixes --- src/shared/schema/mix/actions/dash.ts | 98 +++++++++---------------- src/shared/schema/mix/actions/editor.ts | 65 ++++------------ src/shared/schema/mix/actions/wizard.ts | 71 +++++------------- src/shared/schema/mix/schemas/dash.ts | 57 ++++++++++++++ src/shared/schema/mix/schemas/editor.ts | 54 ++++++++++++++ src/shared/schema/mix/schemas/wizard.ts | 60 +++++++++++++++ src/shared/schema/mix/types/dash.ts | 7 ++ src/shared/schema/mix/types/editor.ts | 5 ++ src/shared/schema/mix/types/index.ts | 1 + 9 files changed, 251 insertions(+), 167 deletions(-) create mode 100644 src/shared/schema/mix/schemas/dash.ts create mode 100644 src/shared/schema/mix/schemas/editor.ts create mode 100644 src/shared/schema/mix/schemas/wizard.ts create mode 100644 src/shared/schema/mix/types/editor.ts diff --git a/src/shared/schema/mix/actions/dash.ts b/src/shared/schema/mix/actions/dash.ts index 40da631f84..2ce4a2efec 100644 --- a/src/shared/schema/mix/actions/dash.ts +++ b/src/shared/schema/mix/actions/dash.ts @@ -1,10 +1,8 @@ import _, {pick} from 'lodash'; import type {DeepNonNullable} from 'utility-types'; -import {z} from 'zod/v4'; import Dash from '../../../../server/components/sdk/dash'; import {DASH_ENTRY_RELEVANT_FIELDS} from '../../../../server/constants'; -import {dashSchema} from '../../../sdk/zod-schemas/dash-api.schema'; import type {ChartsStats} from '../../../types/charts'; import {EntryScope} from '../../../types/common'; import {createAction, createTypedAction} from '../../gateway-utils'; @@ -18,60 +16,34 @@ import { prepareWidgetDatasetData, } from '../helpers/dash'; import { - type CollectChartkitStatsArgs, - type CollectChartkitStatsResponse, - type CollectDashStatsArgs, - type CollectDashStatsResponse, - type GetEntriesDatasetsFieldsArgs, - type GetEntriesDatasetsFieldsResponse, - type GetWidgetsDatasetsFieldsArgs, - type GetWidgetsDatasetsFieldsResponse, + createDashArgsSchema, + createDashResultSchema, + deleteDashArgsSchema, + deleteDashResultSchema, + getDashArgsSchema, + getDashResultSchema, + updateDashArgsSchema, + updateDashResultSchema, +} from '../schemas/dash'; +import type { + CollectChartkitStatsArgs, + CollectChartkitStatsResponse, + CollectDashStatsArgs, + CollectDashStatsResponse, + CreateDashResponse, + GetEntriesDatasetsFieldsArgs, + GetEntriesDatasetsFieldsResponse, + GetWidgetsDatasetsFieldsArgs, + GetWidgetsDatasetsFieldsResponse, + UpdateDashResponse, } from '../types'; -const dashUsSchema = z.object({ - ...dashSchema.shape, - entryId: z.string(), - scope: z.literal(EntryScope.Dash), - public: z.boolean(), - isFavorite: z.boolean(), - createdAt: z.string(), - createdBy: z.string(), - updatedAt: z.string(), - updatedBy: z.string(), - revId: z.string(), - savedId: z.string(), - publishedId: z.string(), - meta: z.record(z.string(), z.string()), - links: z.record(z.string(), z.string()).optional(), - key: z.union([z.null(), z.string()]), - workbookId: z.union([z.null(), z.string()]), - type: z.literal(''), -}); - -const dashUsCreateSchema = z.object({ - ...dashSchema.shape, - workbookId: z.union([z.null(), z.string()]).optional(), - lockToken: z.string().optional(), - mode: z.literal(['publish', 'save']), -}); - -const dashUsUpdateSchema = z.object({ - ...dashSchema.partial().shape, - entryId: z.string(), -}); - export const dashActions = { // WIP __getDashboard__: createTypedAction( { - paramsSchema: z.object({ - dashboardId: z.string(), - revId: z.string().optional(), - includePermissions: z.boolean().optional().default(false), - includeLinks: z.boolean().optional().default(false), - branch: z.literal(['published', 'saved']).optional().default('published'), - }), - resultSchema: dashUsSchema, + paramsSchema: getDashArgsSchema, + resultSchema: getDashResultSchema, }, async (_, args, {headers, ctx}) => { const {dashboardId, includePermissions, includeLinks, branch, revId} = args; @@ -103,11 +75,8 @@ export const dashActions = { // WIP __deleteDashboard__: createTypedAction( { - paramsSchema: z.object({ - dashboardId: z.string(), - lockToken: z.string().optional(), - }), - resultSchema: z.object({}), + paramsSchema: deleteDashArgsSchema, + resultSchema: deleteDashResultSchema, }, async (api, {lockToken, dashboardId}) => { const typedApi = getTypedApi(api); @@ -123,8 +92,8 @@ export const dashActions = { // WIP __updateDashboard__: createTypedAction( { - paramsSchema: dashUsUpdateSchema, - resultSchema: dashUsSchema, + paramsSchema: updateDashArgsSchema, + resultSchema: updateDashResultSchema, }, async (_, args, {headers, ctx}) => { const {entryId} = args; @@ -133,21 +102,24 @@ export const dashActions = { return (await Dash.update(entryId as any, args as any, headers, ctx, I18n, { forceMigrate: true, - })) as unknown as z.infer; + })) as unknown as UpdateDashResponse; }, ), // WIP __createDashboard__: createTypedAction( { - paramsSchema: dashUsCreateSchema, - resultSchema: dashUsSchema, + paramsSchema: createDashArgsSchema, + resultSchema: createDashResultSchema, }, async (_, args, {headers, ctx}) => { const I18n = ctx.get('i18n'); - return (await Dash.create(args as any, headers, ctx, I18n)) as unknown as z.infer< - typeof dashUsSchema - >; + return (await Dash.create( + args as any, + headers, + ctx, + I18n, + )) as unknown as CreateDashResponse; }, ), diff --git a/src/shared/schema/mix/actions/editor.ts b/src/shared/schema/mix/actions/editor.ts index c312fe21b2..e5b0c7d4c3 100644 --- a/src/shared/schema/mix/actions/editor.ts +++ b/src/shared/schema/mix/actions/editor.ts @@ -1,7 +1,4 @@ -import z from 'zod/v4'; - -import {EDITOR_TYPE} from '../../../constants'; -import {DeveloperModeCheckStatus, EntryScope} from '../../../types'; +import {DeveloperModeCheckStatus} from '../../../types'; import {createAction, createTypedAction} from '../../gateway-utils'; import {getTypedApi} from '../../simple-schema'; import type { @@ -12,54 +9,20 @@ import type { } from '../../us/types'; import {getEntryLinks} from '../helpers'; import {validateData} from '../helpers/editor/validation'; - -const editorUsSchema = z.object({ - data: z.union([ - z.object({ - js: z.string(), - url: z.string(), - params: z.string(), - shared: z.string(), - }), - z.object({ - controls: z.string(), - meta: z.string(), - params: z.string(), - prepare: z.string(), - sources: z.string(), - }), - ]), - entryId: z.string(), - scope: z.literal(EntryScope.Widget), - type: z.enum(EDITOR_TYPE), - public: z.boolean(), - isFavorite: z.boolean(), - createdAt: z.string(), - createdBy: z.string(), - updatedAt: z.string(), - updatedBy: z.string(), - revId: z.string(), - savedId: z.string(), - publishedId: z.string(), - meta: z.record(z.string(), z.string()), - links: z.record(z.string(), z.string()).optional(), - key: z.union([z.null(), z.string()]), - workbookId: z.union([z.null(), z.string()]), -}); +import { + deleteEditorChartArgsSchema, + deleteEditorChartResultSchema, + getEditorChartArgsSchema, + getEditorChartResultSchema, +} from '../schemas/editor'; +import type {GetEditorChartResponse} from '../types'; export const editorActions = { // WIP __getEditorChart__: createTypedAction( { - paramsSchema: z.object({ - chardId: z.string(), - workbookId: z.union([z.string(), z.null()]).default(null).optional(), - revId: z.string().optional(), - includePermissions: z.boolean().default(false).optional(), - includeLinks: z.boolean().default(false).optional(), - branch: z.literal(['saved', 'published']).default('published').optional(), - }), - resultSchema: editorUsSchema, + paramsSchema: getEditorChartArgsSchema, + resultSchema: getEditorChartResultSchema, }, async (api, args) => { const {includePermissions, includeLinks, revId, chardId, branch, workbookId} = args; @@ -72,7 +35,7 @@ export const editorActions = { ...(revId ? {revId} : {}), workbookId: workbookId || null, branch: branch || 'published', - }) as unknown as z.infer; + }) as unknown as GetEditorChartResponse; }, ), createEditorChart: createAction( @@ -112,10 +75,8 @@ export const editorActions = { // WIP __deleteEditorChart__: createTypedAction( { - paramsSchema: z.object({ - chartId: z.string(), - }), - resultSchema: z.object({}), + paramsSchema: deleteEditorChartArgsSchema, + resultSchema: deleteEditorChartResultSchema, }, async (api, {chartId}) => { const typedApi = getTypedApi(api); diff --git a/src/shared/schema/mix/actions/wizard.ts b/src/shared/schema/mix/actions/wizard.ts index 42bec849bd..2d09c336f0 100644 --- a/src/shared/schema/mix/actions/wizard.ts +++ b/src/shared/schema/mix/actions/wizard.ts @@ -1,43 +1,24 @@ -import z from 'zod/v4'; - import {USProvider} from '../../../../server/components/charts-engine/components/storage/united-storage/provider'; -import {v12ChartsConfigSchema} from '../../../sdk/zod-schemas/wizard-chart-api.schema'; -import {EntryScope, WizardType} from '../../../types'; +import {EntryScope} from '../../../types'; import {createTypedAction} from '../../gateway-utils'; import {getTypedApi} from '../../simple-schema'; - -const wizardUsSchema = z.object({ - data: v12ChartsConfigSchema, - entryId: z.string(), - scope: z.literal(EntryScope.Widget), - type: z.enum(WizardType), - public: z.boolean(), - isFavorite: z.boolean(), - createdAt: z.string(), - createdBy: z.string(), - updatedAt: z.string(), - updatedBy: z.string(), - revId: z.string(), - savedId: z.string(), - publishedId: z.string(), - meta: z.record(z.string(), z.string()), - links: z.record(z.string(), z.string()).optional(), - key: z.union([z.null(), z.string()]), - workbookId: z.union([z.null(), z.string()]), -}); +import { + createWizardChartArgsSchema, + createWizardChartResultSchema, + deleteWizardChartArgsSchema, + deleteWizardChartResultSchema, + getWizardChartArgsSchema, + getWizardChartResultSchema, + updateWizardChartArgsSchema, + updateWizardChartResultSchema, +} from '../schemas/wizard'; export const wizardActions = { // WIP __getWizardChart__: createTypedAction( { - paramsSchema: z.object({ - chardId: z.string(), - unreleased: z.boolean().default(false).optional(), - revId: z.string().optional(), - includePermissions: z.boolean().default(false).optional(), - includeLinks: z.boolean().default(false).optional(), - }), - resultSchema: wizardUsSchema, + paramsSchema: getWizardChartArgsSchema, + resultSchema: getWizardChartResultSchema, }, async (_, args, {ctx, headers}) => { const {includePermissions, includeLinks, unreleased, revId, chardId} = args; @@ -57,15 +38,8 @@ export const wizardActions = { // WIP __createWizardChart__: createTypedAction( { - paramsSchema: z.object({ - entryId: z.string(), - data: v12ChartsConfigSchema, - key: z.string(), - workbookId: z.union([z.string(), z.null()]).optional(), - type: z.enum(WizardType).optional(), - name: z.string(), - }), - resultSchema: wizardUsSchema, + paramsSchema: createWizardChartArgsSchema, + resultSchema: createWizardChartResultSchema, }, async (_, args, {ctx, headers}) => { const {data, type, key, workbookId, name} = args; @@ -86,13 +60,8 @@ export const wizardActions = { // WIP __updateWizardChart__: createTypedAction( { - paramsSchema: z.object({ - entryId: z.string(), - revId: z.string().optional(), - data: v12ChartsConfigSchema, - type: z.enum(WizardType).optional(), - }), - resultSchema: wizardUsSchema, + paramsSchema: updateWizardChartArgsSchema, + resultSchema: updateWizardChartResultSchema, }, async (_, args, {ctx, headers}) => { const {entryId, revId, data, type} = args; @@ -111,10 +80,8 @@ export const wizardActions = { // WIP __deleteWizardChart__: createTypedAction( { - paramsSchema: z.object({ - chartId: z.string(), - }), - resultSchema: z.object({}), + paramsSchema: deleteWizardChartArgsSchema, + resultSchema: deleteWizardChartResultSchema, }, async (api, {chartId}) => { const typedApi = getTypedApi(api); diff --git a/src/shared/schema/mix/schemas/dash.ts b/src/shared/schema/mix/schemas/dash.ts new file mode 100644 index 0000000000..640f32e70a --- /dev/null +++ b/src/shared/schema/mix/schemas/dash.ts @@ -0,0 +1,57 @@ +import z from 'zod/v4'; + +import {EntryScope} from '../../..'; +import {dashSchema} from '../../../sdk/zod-schemas/dash-api.schema'; + +export const deleteDashArgsSchema = z.object({ + dashboardId: z.string(), + lockToken: z.string().optional(), +}); + +export const deleteDashResultSchema = z.object({}); + +const dashUsSchema = z.object({ + ...dashSchema.shape, + entryId: z.string(), + scope: z.literal(EntryScope.Dash), + public: z.boolean(), + isFavorite: z.boolean(), + createdAt: z.string(), + createdBy: z.string(), + updatedAt: z.string(), + updatedBy: z.string(), + revId: z.string(), + savedId: z.string(), + publishedId: z.string(), + meta: z.record(z.string(), z.string()), + links: z.record(z.string(), z.string()).optional(), + key: z.union([z.null(), z.string()]), + workbookId: z.union([z.null(), z.string()]), + type: z.literal(''), +}); + +export const updateDashArgsSchema = z.object({ + ...dashSchema.partial().shape, + entryId: z.string(), +}); + +export const updateDashResultSchema = dashUsSchema; + +export const createDashArgsSchema = z.object({ + ...dashSchema.shape, + workbookId: z.union([z.null(), z.string()]).optional(), + lockToken: z.string().optional(), + mode: z.literal(['publish', 'save']), +}); + +export const createDashResultSchema = dashUsSchema; + +export const getDashArgsSchema = z.object({ + dashboardId: z.string(), + revId: z.string().optional(), + includePermissions: z.boolean().optional().default(false), + includeLinks: z.boolean().optional().default(false), + branch: z.literal(['published', 'saved']).optional().default('published'), +}); + +export const getDashResultSchema = dashUsSchema; diff --git a/src/shared/schema/mix/schemas/editor.ts b/src/shared/schema/mix/schemas/editor.ts new file mode 100644 index 0000000000..270031563e --- /dev/null +++ b/src/shared/schema/mix/schemas/editor.ts @@ -0,0 +1,54 @@ +import z from 'zod/v4'; + +import {EDITOR_TYPE, EntryScope} from '../../..'; + +export const deleteEditorChartArgsSchema = z.object({ + chartId: z.string(), +}); + +export const deleteEditorChartResultSchema = z.object({}); + +export const getEditorChartArgsSchema = z.object({ + chardId: z.string(), + workbookId: z.union([z.string(), z.null()]).default(null).optional(), + revId: z.string().optional(), + includePermissions: z.boolean().default(false).optional(), + includeLinks: z.boolean().default(false).optional(), + branch: z.literal(['saved', 'published']).default('published').optional(), +}); + +const editorUsSchema = z.object({ + data: z.union([ + z.object({ + js: z.string(), + url: z.string(), + params: z.string(), + shared: z.string(), + }), + z.object({ + controls: z.string(), + meta: z.string(), + params: z.string(), + prepare: z.string(), + sources: z.string(), + }), + ]), + entryId: z.string(), + scope: z.literal(EntryScope.Widget), + type: z.enum(EDITOR_TYPE), + public: z.boolean(), + isFavorite: z.boolean(), + createdAt: z.string(), + createdBy: z.string(), + updatedAt: z.string(), + updatedBy: z.string(), + revId: z.string(), + savedId: z.string(), + publishedId: z.string(), + meta: z.record(z.string(), z.string()), + links: z.record(z.string(), z.string()).optional(), + key: z.union([z.null(), z.string()]), + workbookId: z.union([z.null(), z.string()]), +}); + +export const getEditorChartResultSchema = editorUsSchema; diff --git a/src/shared/schema/mix/schemas/wizard.ts b/src/shared/schema/mix/schemas/wizard.ts new file mode 100644 index 0000000000..386ac0560b --- /dev/null +++ b/src/shared/schema/mix/schemas/wizard.ts @@ -0,0 +1,60 @@ +import z from 'zod/v4'; + +import {EntryScope, WizardType} from '../../..'; +import {v12ChartsConfigSchema} from '../../../sdk/zod-schemas/wizard-chart-api.schema'; + +export const getWizardChartArgsSchema = z.object({ + chardId: z.string(), + unreleased: z.boolean().default(false).optional(), + revId: z.string().optional(), + includePermissions: z.boolean().default(false).optional(), + includeLinks: z.boolean().default(false).optional(), +}); + +const wizardUsSchema = z.object({ + data: v12ChartsConfigSchema, + entryId: z.string(), + scope: z.literal(EntryScope.Widget), + type: z.enum(WizardType), + public: z.boolean(), + isFavorite: z.boolean(), + createdAt: z.string(), + createdBy: z.string(), + updatedAt: z.string(), + updatedBy: z.string(), + revId: z.string(), + savedId: z.string(), + publishedId: z.string(), + meta: z.record(z.string(), z.string()), + links: z.record(z.string(), z.string()).optional(), + key: z.union([z.null(), z.string()]), + workbookId: z.union([z.null(), z.string()]), +}); + +export const getWizardChartResultSchema = wizardUsSchema; + +export const createWizardChartArgsSchema = z.object({ + entryId: z.string(), + data: v12ChartsConfigSchema, + key: z.string(), + workbookId: z.union([z.string(), z.null()]).optional(), + type: z.enum(WizardType).optional(), + name: z.string(), +}); + +export const createWizardChartResultSchema = wizardUsSchema; + +export const updateWizardChartArgsSchema = z.object({ + entryId: z.string(), + revId: z.string().optional(), + data: v12ChartsConfigSchema, + type: z.enum(WizardType).optional(), +}); + +export const updateWizardChartResultSchema = wizardUsSchema; + +export const deleteWizardChartArgsSchema = z.object({ + chartId: z.string(), +}); + +export const deleteWizardChartResultSchema = z.object({}); diff --git a/src/shared/schema/mix/types/dash.ts b/src/shared/schema/mix/types/dash.ts index 6a947623e5..649e267ad9 100644 --- a/src/shared/schema/mix/types/dash.ts +++ b/src/shared/schema/mix/types/dash.ts @@ -1,6 +1,9 @@ +import type z from 'zod/v4'; + import type {WizardVisualizationId} from '../../../constants'; import type {ChartsStats, DashStats, WorkbookId} from '../../../types'; import type {GetEntriesEntryResponse} from '../../us/types'; +import type {createDashResultSchema, updateDashResultSchema} from '../schemas/dash'; export type CollectDashStatsResponse = { status: string; @@ -51,3 +54,7 @@ export type GetWidgetsDatasetsFieldsArgs = { entriesIds: string[]; workbookId: WorkbookId; }; + +export type UpdateDashResponse = z.infer; + +export type CreateDashResponse = z.infer; diff --git a/src/shared/schema/mix/types/editor.ts b/src/shared/schema/mix/types/editor.ts new file mode 100644 index 0000000000..f645cc0149 --- /dev/null +++ b/src/shared/schema/mix/types/editor.ts @@ -0,0 +1,5 @@ +import type z from 'zod/v4'; + +import type {getEditorChartResultSchema} from '../schemas/editor'; + +export type GetEditorChartResponse = z.infer; diff --git a/src/shared/schema/mix/types/index.ts b/src/shared/schema/mix/types/index.ts index 90757ac7a7..7de86b74d3 100644 --- a/src/shared/schema/mix/types/index.ts +++ b/src/shared/schema/mix/types/index.ts @@ -2,3 +2,4 @@ export * from './navigation'; export * from './entries'; export * from './markdown'; export * from './dash'; +export * from './editor'; From d84109fe734ed40492257c2e2f48060366714cb7 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Mon, 15 Sep 2025 11:13:50 +0300 Subject: [PATCH 30/40] Fixes --- src/server/components/public-api/constants.ts | 35 ++++--------------- src/shared/schema/bi/actions/connections.ts | 22 +++++++----- src/shared/schema/bi/actions/datasets.ts | 4 +-- src/shared/schema/bi/schemas/connections.ts | 7 ++++ src/shared/schema/bi/schemas/datasets.ts | 8 ++++- src/shared/schema/bi/types/connections.ts | 7 ++-- 6 files changed, 41 insertions(+), 42 deletions(-) create mode 100644 src/shared/schema/bi/schemas/connections.ts diff --git a/src/server/components/public-api/constants.ts b/src/server/components/public-api/constants.ts index 114d0915ac..dd5256d467 100644 --- a/src/server/components/public-api/constants.ts +++ b/src/server/components/public-api/constants.ts @@ -27,27 +27,6 @@ export const PUBLIC_API_PROXY_MAP = { // tags: [ApiTag.Navigation], // }, // }, - // getStructureItems: { - // resolve: (api) => api.us.getStructureItems, - // openApi: { - // summary: 'Get structure list', - // tags: [ApiTag.Navigation], - // }, - // }, - // createWorkbook: { - // resolve: (api) => api.us.createWorkbook, - // openApi: { - // summary: 'Create workbook', - // tags: [ApiTag.Navigation], - // }, - // }, - // createCollection: { - // resolve: (api) => api.us.createCollection, - // openApi: { - // summary: 'Create collection', - // tags: [ApiTag.Navigation], - // }, - // }, // connection // getConnection: { // resolve: (api) => api.bi.getConnection, @@ -70,13 +49,13 @@ export const PUBLIC_API_PROXY_MAP = { // tags: [ApiTag.Connection], // }, // }, - // deleteConnection: { - // resolve: (api) => api.bi.deleteConnection, - // openApi: { - // summary: 'Delete connection', - // tags: [ApiTag.Connection], - // }, - // }, + deleteConnection: { + resolve: (api) => api.bi.deleteConnection, + openApi: { + summary: 'Delete connection', + tags: [ApiTag.Connection], + }, + }, // dataset getDataset: { resolve: (api) => api.bi.getDatasetByVersion, diff --git a/src/shared/schema/bi/actions/connections.ts b/src/shared/schema/bi/actions/connections.ts index c0fcc896eb..191c58dce3 100644 --- a/src/shared/schema/bi/actions/connections.ts +++ b/src/shared/schema/bi/actions/connections.ts @@ -1,12 +1,11 @@ import {US_MASTER_TOKEN_HEADER, WORKBOOK_ID_HEADER} from '../../../constants'; -import {createAction} from '../../gateway-utils'; +import {createAction, createTypedAction} from '../../gateway-utils'; import {filterUrlFragment} from '../../utils'; import {transformConnectionResponseError} from '../helpers'; +import {deleteConnectionArgsSchema, deleteConnectionResultSchema} from '../schemas/connections'; import type { CreateConnectionArgs, CreateConnectionResponse, - DeleteConnectionArgs, - DeleteConnectionResponse, EnsureUploadRobotArgs, EnsureUploadRobotResponse, ExportConnectionArgs, @@ -95,11 +94,18 @@ export const actions = { params: ({connectionId: _connectionId, ...body}, headers) => ({body, headers}), transformResponseError: transformConnectionResponseError, }), - deleteConnection: createAction({ - method: 'DELETE', - path: ({connectionId}) => `${PATH_PREFIX}/connections/${filterUrlFragment(connectionId)}`, - params: (_, headers) => ({headers}), - }), + deleteConnection: createTypedAction( + { + paramsSchema: deleteConnectionArgsSchema, + resultSchema: deleteConnectionResultSchema, + }, + { + method: 'DELETE', + path: ({connectionId}) => + `${PATH_PREFIX}/connections/${filterUrlFragment(connectionId)}`, + params: (_, headers) => ({headers}), + }, + ), getConnectionSources: createAction({ method: 'GET', path: ({connectionId}) => `${PATH_PREFIX}/connections/${connectionId}/info/sources`, diff --git a/src/shared/schema/bi/actions/datasets.ts b/src/shared/schema/bi/actions/datasets.ts index 011d1d9671..8365e83b75 100644 --- a/src/shared/schema/bi/actions/datasets.ts +++ b/src/shared/schema/bi/actions/datasets.ts @@ -4,7 +4,6 @@ import { US_MASTER_TOKEN_HEADER, WORKBOOK_ID_HEADER, } from '../../../constants'; -import {datasetSchema} from '../../../sdk/zod-schemas/dataset-api.schema'; import {createAction, createTypedAction} from '../../gateway-utils'; import {filterUrlFragment} from '../../utils'; import { @@ -19,6 +18,7 @@ import { deleteDatasetArgsSchema, deleteDatasetResultSchema, getDatasetByVersionArgsSchema, + getDatasetByVersionResultSchema, updateDatasetArgsSchema, updateDatasetResultSchema, } from '../schemas'; @@ -68,7 +68,7 @@ export const actions = { getDatasetByVersion: createTypedAction( { paramsSchema: getDatasetByVersionArgsSchema, - resultSchema: datasetSchema, + resultSchema: getDatasetByVersionResultSchema, }, { method: 'GET', diff --git a/src/shared/schema/bi/schemas/connections.ts b/src/shared/schema/bi/schemas/connections.ts new file mode 100644 index 0000000000..250f0518bc --- /dev/null +++ b/src/shared/schema/bi/schemas/connections.ts @@ -0,0 +1,7 @@ +import z from 'zod/v4'; + +export const deleteConnectionArgsSchema = z.object({ + connectionId: z.string(), +}); + +export const deleteConnectionResultSchema = z.unknown(); diff --git a/src/shared/schema/bi/schemas/datasets.ts b/src/shared/schema/bi/schemas/datasets.ts index cb8abddf1f..3f9bcc5a9f 100644 --- a/src/shared/schema/bi/schemas/datasets.ts +++ b/src/shared/schema/bi/schemas/datasets.ts @@ -1,6 +1,10 @@ import z from 'zod/v4'; -import {datasetBodySchema, datasetOptionsSchema} from '../../../sdk/zod-schemas/dataset-api.schema'; +import { + datasetBodySchema, + datasetOptionsSchema, + datasetSchema, +} from '../../../sdk/zod-schemas/dataset-api.schema'; const createDatasetDefaultArgsSchema = z.object({ name: z.string(), @@ -45,3 +49,5 @@ export const getDatasetByVersionArgsSchema = z.object({ workbookId: z.union([z.null(), z.string()]), rev_id: z.string().optional(), }); + +export const getDatasetByVersionResultSchema = datasetSchema; diff --git a/src/shared/schema/bi/types/connections.ts b/src/shared/schema/bi/types/connections.ts index 74f31627b8..bd8999f489 100644 --- a/src/shared/schema/bi/types/connections.ts +++ b/src/shared/schema/bi/types/connections.ts @@ -1,3 +1,5 @@ +import type z from 'zod/v4'; + import type {ConnectorType} from '../../../constants'; import type { ConnectionData, @@ -5,6 +7,7 @@ import type { ConnectionTypedQueryApiResponse, TransferNotification, } from '../../../types'; +import type {deleteConnectionResultSchema} from '../schemas/connections'; import type {WorkbookIdArg} from './common'; @@ -56,9 +59,7 @@ export type GetAvailableCountersResponse = { export type GetAvailableCountersArgs = BaseArgs; -export type DeleteConnectionResponse = unknown; - -export type DeleteConnectionArgs = BaseArgs; +export type DeleteConnectionResponse = z.infer; export type GetConnectorsResponse = { /** @deprecated use `sections` & `uncategorized` fields instead */ From d4a83485bd2c9d6049d512a61463ed548c99d13e Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Mon, 15 Sep 2025 13:25:05 +0300 Subject: [PATCH 31/40] Review fixes --- src/server/components/public-api/types.ts | 10 ++++------ .../public-api/utils/init-public-api-swagger.ts | 4 ++-- src/server/constants/public-api.ts | 2 -- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/server/components/public-api/types.ts b/src/server/components/public-api/types.ts index 529334bcee..91d6873434 100644 --- a/src/server/components/public-api/types.ts +++ b/src/server/components/public-api/types.ts @@ -1,19 +1,17 @@ import type {Request, Response} from '@gravity-ui/expresskit'; -import type {ApiWithRoot, SchemasByScope} from '@gravity-ui/gateway'; +import type {ApiWithRoot, GatewayActionUnaryResponse, SchemasByScope} from '@gravity-ui/gateway'; import type {DatalensGatewaySchemas} from '../../types/gateway'; import type {SecuritySchemeObject} from '../api-docs'; -type HeadersType = Record; - export type PublicApiRpcMap = Record< string, Record< string, { - resolve: (api: ApiWithRoot) => any; - headers?: (req: Request, headers: HeadersType) => HeadersType; - args?: (req: Request) => Promise | object; + resolve: ( + api: ApiWithRoot, + ) => (params: any) => Promise>; openApi: { summary: string; tags?: string[]; diff --git a/src/server/components/public-api/utils/init-public-api-swagger.ts b/src/server/components/public-api/utils/init-public-api-swagger.ts index 85e2ae1c16..a4e6ed57c7 100644 --- a/src/server/components/public-api/utils/init-public-api-swagger.ts +++ b/src/server/components/public-api/utils/init-public-api-swagger.ts @@ -30,8 +30,8 @@ export const initPublicApiSwagger = ( const generateDocumentParams: OpenAPIObjectConfigV31 = { openapi: '3.1.0', info: { - version: `${config.appVersion}`, - title: `UI API `, + version: `v0`, + title: `DataLens API `, description: [installationText, envText, descriptionText].join('
'), }, servers: [{url: '/'}], diff --git a/src/server/constants/public-api.ts b/src/server/constants/public-api.ts index 6c1bbfb0a3..105acced81 100644 --- a/src/server/constants/public-api.ts +++ b/src/server/constants/public-api.ts @@ -1,3 +1 @@ -export const PUBLIC_API_RPC_ERROR_CODE = 'ERR.UI_API.PUBLIC-API.FAILED_RPC_PROXY'; - export const PUBLIC_API_ORG_ID_HEADER = 'x-dl-org-id'; From 20c64a681303c5479c71f2cabdf77276129f7d95 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Mon, 15 Sep 2025 13:57:41 +0300 Subject: [PATCH 32/40] Remove not ready schemas --- src/server/components/public-api/constants.ts | 107 ++---------------- src/shared/schema/bi/schemas/datasets.ts | 6 +- src/shared/schema/mix/actions/dash.ts | 83 +------------- src/shared/schema/mix/actions/editor.ts | 28 +---- src/shared/schema/mix/actions/wizard.ts | 76 +------------ src/shared/schema/mix/schemas/dash.ts | 49 -------- src/shared/schema/mix/schemas/editor.ts | 47 -------- src/shared/schema/mix/schemas/wizard.ts | 53 --------- src/shared/schema/mix/types/dash.ts | 7 -- src/shared/schema/mix/types/editor.ts | 5 - .../__tests__/dash-api.schema.test.ts | 4 +- .../dash.ts} | 3 +- .../dataset.ts} | 55 +-------- .../wizard.ts} | 10 +- 14 files changed, 28 insertions(+), 505 deletions(-) delete mode 100644 src/shared/schema/mix/types/editor.ts rename src/shared/{sdk => }/zod-schemas/__tests__/dash-api.schema.test.ts (99%) rename src/shared/{sdk/zod-schemas/dash-api.schema.ts => zod-schemas/dash.ts} (99%) rename src/shared/{sdk/zod-schemas/dataset-api.schema.ts => zod-schemas/dataset.ts} (86%) rename src/shared/{sdk/zod-schemas/wizard-chart-api.schema.ts => zod-schemas/wizard.ts} (98%) diff --git a/src/server/components/public-api/constants.ts b/src/server/components/public-api/constants.ts index dd5256d467..ebe99cde13 100644 --- a/src/server/components/public-api/constants.ts +++ b/src/server/components/public-api/constants.ts @@ -9,7 +9,6 @@ export const PUBLIC_API_URL = '/rpc/:version/:action'; export const PUBLIC_API_ROUTE = `${PUBLIC_API_HTTP_METHOD} ${PUBLIC_API_URL}`; enum ApiTag { - Navigation = 'Navigation', Connection = 'Connection', Dataset = 'Dataset', Wizard = 'Wizard', @@ -19,36 +18,7 @@ enum ApiTag { export const PUBLIC_API_PROXY_MAP = { v0: { - // navigation - // getNavigationList: { - // resolve: (api) => api.mix.getNavigationList, - // openApi: { - // summary: 'Get navigation list', - // tags: [ApiTag.Navigation], - // }, - // }, - // connection - // getConnection: { - // resolve: (api) => api.bi.getConnection, - // openApi: { - // summary: 'Get connection', - // tags: [ApiTag.Connection], - // }, - // }, - // updateConnection: { - // resolve: (api) => api.bi.updateConnection, - // openApi: { - // summary: 'Update connection', - // tags: [ApiTag.Connection], - // }, - // }, - // createConnection: { - // resolve: (api) => api.bi.createConnection, - // openApi: { - // summary: 'Create connection', - // tags: [ApiTag.Connection], - // }, - // }, + // Connection deleteConnection: { resolve: (api) => api.bi.deleteConnection, openApi: { @@ -56,7 +26,8 @@ export const PUBLIC_API_PROXY_MAP = { tags: [ApiTag.Connection], }, }, - // dataset + + // Dataset getDataset: { resolve: (api) => api.bi.getDatasetByVersion, openApi: { @@ -85,28 +56,8 @@ export const PUBLIC_API_PROXY_MAP = { tags: [ApiTag.Dataset], }, }, - // wizard - getWizardChart: { - resolve: (api) => api.mix.__getWizardChart__, - openApi: { - summary: 'Get wizard chart', - tags: [ApiTag.Wizard], - }, - }, - updateWizardChart: { - resolve: (api) => api.mix.__updateWizardChart__, - openApi: { - summary: 'Update wizard chart', - tags: [ApiTag.Wizard], - }, - }, - createWizardChart: { - resolve: (api) => api.mix.__createWizardChart__, - openApi: { - summary: 'Create wizard chart', - tags: [ApiTag.Wizard], - }, - }, + + // Wizard deleteWizardChart: { resolve: (api) => api.mix.__deleteWizardChart__, openApi: { @@ -114,28 +65,8 @@ export const PUBLIC_API_PROXY_MAP = { tags: [ApiTag.Wizard], }, }, - // editor - getEditorChart: { - resolve: (api) => api.mix.__getEditorChart__, - openApi: { - summary: 'Get editor chart', - tags: [ApiTag.Editor], - }, - }, - // updateEditorChart: { - // resolve: (api) => api.mix.updateEditorChart, - // openApi: { - // summary: 'Update editor chart', - // tags: [ApiTag.Editor], - // }, - // }, - // createEditorChart: { - // resolve: (api) => api.mix.createEditorChart, - // openApi: { - // summary: 'Create editor chart', - // tags: [ApiTag.Editor], - // }, - // }, + + // Editor deleteEditorChart: { resolve: (api) => api.mix.__deleteEditorChart__, openApi: { @@ -143,28 +74,8 @@ export const PUBLIC_API_PROXY_MAP = { tags: [ApiTag.Editor], }, }, - // Dash - getDashboard: { - resolve: (api) => api.mix.__getDashboard__, - openApi: { - summary: 'Get dashboard', - tags: [ApiTag.Dashboard], - }, - }, - updateDashboard: { - resolve: (api) => api.mix.__updateDashboard__, - openApi: { - summary: 'Delete dashboard', - tags: [ApiTag.Dashboard], - }, - }, - createDashboard: { - resolve: (api) => api.mix.__createDashboard__, - openApi: { - summary: 'Create dashboard', - tags: [ApiTag.Dashboard], - }, - }, + + // Dashboard deleteDashboard: { resolve: (api) => api.mix.__deleteDashboard__, openApi: { diff --git a/src/shared/schema/bi/schemas/datasets.ts b/src/shared/schema/bi/schemas/datasets.ts index 3f9bcc5a9f..fcf0f7a4b2 100644 --- a/src/shared/schema/bi/schemas/datasets.ts +++ b/src/shared/schema/bi/schemas/datasets.ts @@ -1,10 +1,6 @@ import z from 'zod/v4'; -import { - datasetBodySchema, - datasetOptionsSchema, - datasetSchema, -} from '../../../sdk/zod-schemas/dataset-api.schema'; +import {datasetBodySchema, datasetOptionsSchema, datasetSchema} from '../../../zod-schemas/dataset'; const createDatasetDefaultArgsSchema = z.object({ name: z.string(), diff --git a/src/shared/schema/mix/actions/dash.ts b/src/shared/schema/mix/actions/dash.ts index 2ce4a2efec..2529664ac9 100644 --- a/src/shared/schema/mix/actions/dash.ts +++ b/src/shared/schema/mix/actions/dash.ts @@ -1,10 +1,6 @@ -import _, {pick} from 'lodash'; import type {DeepNonNullable} from 'utility-types'; -import Dash from '../../../../server/components/sdk/dash'; -import {DASH_ENTRY_RELEVANT_FIELDS} from '../../../../server/constants'; import type {ChartsStats} from '../../../types/charts'; -import {EntryScope} from '../../../types/common'; import {createAction, createTypedAction} from '../../gateway-utils'; import {getTypedApi} from '../../simple-schema'; import {getEntryVisualizationType} from '../helpers'; @@ -15,63 +11,19 @@ import { prepareDatasetData, prepareWidgetDatasetData, } from '../helpers/dash'; -import { - createDashArgsSchema, - createDashResultSchema, - deleteDashArgsSchema, - deleteDashResultSchema, - getDashArgsSchema, - getDashResultSchema, - updateDashArgsSchema, - updateDashResultSchema, -} from '../schemas/dash'; +import {deleteDashArgsSchema, deleteDashResultSchema} from '../schemas/dash'; import type { CollectChartkitStatsArgs, CollectChartkitStatsResponse, CollectDashStatsArgs, CollectDashStatsResponse, - CreateDashResponse, GetEntriesDatasetsFieldsArgs, GetEntriesDatasetsFieldsResponse, GetWidgetsDatasetsFieldsArgs, GetWidgetsDatasetsFieldsResponse, - UpdateDashResponse, } from '../types'; export const dashActions = { - // WIP - __getDashboard__: createTypedAction( - { - paramsSchema: getDashArgsSchema, - resultSchema: getDashResultSchema, - }, - async (_, args, {headers, ctx}) => { - const {dashboardId, includePermissions, includeLinks, branch, revId} = args; - - if (!dashboardId || dashboardId === 'null') { - throw new Error(`Not found ${dashboardId} id`); - } - - const result = await Dash.read( - dashboardId, - { - includePermissions: includePermissions ? includePermissions?.toString() : '0', - includeLinks: includeLinks ? includeLinks?.toString() : '0', - ...(branch ? {branch} : {branch: 'published'}), - ...(revId ? {revId} : {}), - }, - headers, - ctx, - {forceMigrate: true}, - ); - - if (result.scope !== EntryScope.Dash) { - throw new Error('No entry found'); - } - - return pick(result, DASH_ENTRY_RELEVANT_FIELDS) as any; - }, - ), // WIP __deleteDashboard__: createTypedAction( { @@ -89,39 +41,6 @@ export const dashActions = { return {}; }, ), - // WIP - __updateDashboard__: createTypedAction( - { - paramsSchema: updateDashArgsSchema, - resultSchema: updateDashResultSchema, - }, - async (_, args, {headers, ctx}) => { - const {entryId} = args; - - const I18n = ctx.get('i18n'); - - return (await Dash.update(entryId as any, args as any, headers, ctx, I18n, { - forceMigrate: true, - })) as unknown as UpdateDashResponse; - }, - ), - // WIP - __createDashboard__: createTypedAction( - { - paramsSchema: createDashArgsSchema, - resultSchema: createDashResultSchema, - }, - async (_, args, {headers, ctx}) => { - const I18n = ctx.get('i18n'); - - return (await Dash.create( - args as any, - headers, - ctx, - I18n, - )) as unknown as CreateDashResponse; - }, - ), collectDashStats: createAction( async (_, args, {ctx}) => { diff --git a/src/shared/schema/mix/actions/editor.ts b/src/shared/schema/mix/actions/editor.ts index e5b0c7d4c3..7309bfba65 100644 --- a/src/shared/schema/mix/actions/editor.ts +++ b/src/shared/schema/mix/actions/editor.ts @@ -9,35 +9,9 @@ import type { } from '../../us/types'; import {getEntryLinks} from '../helpers'; import {validateData} from '../helpers/editor/validation'; -import { - deleteEditorChartArgsSchema, - deleteEditorChartResultSchema, - getEditorChartArgsSchema, - getEditorChartResultSchema, -} from '../schemas/editor'; -import type {GetEditorChartResponse} from '../types'; +import {deleteEditorChartArgsSchema, deleteEditorChartResultSchema} from '../schemas/editor'; export const editorActions = { - // WIP - __getEditorChart__: createTypedAction( - { - paramsSchema: getEditorChartArgsSchema, - resultSchema: getEditorChartResultSchema, - }, - async (api, args) => { - const {includePermissions, includeLinks, revId, chardId, branch, workbookId} = args; - const typedApi = getTypedApi(api); - - return typedApi.us.getEntry({ - entryId: chardId, - includePermissionsInfo: includePermissions ? Boolean(includePermissions) : false, - includeLinks: includeLinks ? Boolean(includeLinks) : false, - ...(revId ? {revId} : {}), - workbookId: workbookId || null, - branch: branch || 'published', - }) as unknown as GetEditorChartResponse; - }, - ), createEditorChart: createAction( async (api, args, {ctx}) => { const {checkRequestForDeveloperModeAccess} = ctx.get('gateway'); diff --git a/src/shared/schema/mix/actions/wizard.ts b/src/shared/schema/mix/actions/wizard.ts index 2d09c336f0..a6618be158 100644 --- a/src/shared/schema/mix/actions/wizard.ts +++ b/src/shared/schema/mix/actions/wizard.ts @@ -1,82 +1,8 @@ -import {USProvider} from '../../../../server/components/charts-engine/components/storage/united-storage/provider'; -import {EntryScope} from '../../../types'; import {createTypedAction} from '../../gateway-utils'; import {getTypedApi} from '../../simple-schema'; -import { - createWizardChartArgsSchema, - createWizardChartResultSchema, - deleteWizardChartArgsSchema, - deleteWizardChartResultSchema, - getWizardChartArgsSchema, - getWizardChartResultSchema, - updateWizardChartArgsSchema, - updateWizardChartResultSchema, -} from '../schemas/wizard'; +import {deleteWizardChartArgsSchema, deleteWizardChartResultSchema} from '../schemas/wizard'; export const wizardActions = { - // WIP - __getWizardChart__: createTypedAction( - { - paramsSchema: getWizardChartArgsSchema, - resultSchema: getWizardChartResultSchema, - }, - async (_, args, {ctx, headers}) => { - const {includePermissions, includeLinks, unreleased, revId, chardId} = args; - - const result = await USProvider.retrieveParsedWizardChart(ctx, { - id: chardId, - includePermissionsInfo: includePermissions ? includePermissions?.toString() : '0', - includeLinks: includeLinks ? includeLinks?.toString() : '0', - ...(revId ? {revId} : {}), - ...(unreleased ? {unreleased} : {unreleased: false}), - headers, - }); - - return result as any; - }, - ), - // WIP - __createWizardChart__: createTypedAction( - { - paramsSchema: createWizardChartArgsSchema, - resultSchema: createWizardChartResultSchema, - }, - async (_, args, {ctx, headers}) => { - const {data, type, key, workbookId, name} = args; - - const result = await USProvider.create(ctx, { - type, - data, - key, - name, - scope: EntryScope.Widget, - ...(workbookId ? {workbookId} : {workbookId: null}), - headers, - }); - - return result as any; - }, - ), - // WIP - __updateWizardChart__: createTypedAction( - { - paramsSchema: updateWizardChartArgsSchema, - resultSchema: updateWizardChartResultSchema, - }, - async (_, args, {ctx, headers}) => { - const {entryId, revId, data, type} = args; - - const result = await USProvider.update(ctx, { - entryId, - ...(revId ? {revId} : {}), - ...(type ? {type} : {}), - data, - headers, - }); - - return result as any; - }, - ), // WIP __deleteWizardChart__: createTypedAction( { diff --git a/src/shared/schema/mix/schemas/dash.ts b/src/shared/schema/mix/schemas/dash.ts index 640f32e70a..74e3f8c236 100644 --- a/src/shared/schema/mix/schemas/dash.ts +++ b/src/shared/schema/mix/schemas/dash.ts @@ -1,57 +1,8 @@ import z from 'zod/v4'; -import {EntryScope} from '../../..'; -import {dashSchema} from '../../../sdk/zod-schemas/dash-api.schema'; - export const deleteDashArgsSchema = z.object({ dashboardId: z.string(), lockToken: z.string().optional(), }); export const deleteDashResultSchema = z.object({}); - -const dashUsSchema = z.object({ - ...dashSchema.shape, - entryId: z.string(), - scope: z.literal(EntryScope.Dash), - public: z.boolean(), - isFavorite: z.boolean(), - createdAt: z.string(), - createdBy: z.string(), - updatedAt: z.string(), - updatedBy: z.string(), - revId: z.string(), - savedId: z.string(), - publishedId: z.string(), - meta: z.record(z.string(), z.string()), - links: z.record(z.string(), z.string()).optional(), - key: z.union([z.null(), z.string()]), - workbookId: z.union([z.null(), z.string()]), - type: z.literal(''), -}); - -export const updateDashArgsSchema = z.object({ - ...dashSchema.partial().shape, - entryId: z.string(), -}); - -export const updateDashResultSchema = dashUsSchema; - -export const createDashArgsSchema = z.object({ - ...dashSchema.shape, - workbookId: z.union([z.null(), z.string()]).optional(), - lockToken: z.string().optional(), - mode: z.literal(['publish', 'save']), -}); - -export const createDashResultSchema = dashUsSchema; - -export const getDashArgsSchema = z.object({ - dashboardId: z.string(), - revId: z.string().optional(), - includePermissions: z.boolean().optional().default(false), - includeLinks: z.boolean().optional().default(false), - branch: z.literal(['published', 'saved']).optional().default('published'), -}); - -export const getDashResultSchema = dashUsSchema; diff --git a/src/shared/schema/mix/schemas/editor.ts b/src/shared/schema/mix/schemas/editor.ts index 270031563e..96f89fee9c 100644 --- a/src/shared/schema/mix/schemas/editor.ts +++ b/src/shared/schema/mix/schemas/editor.ts @@ -1,54 +1,7 @@ import z from 'zod/v4'; -import {EDITOR_TYPE, EntryScope} from '../../..'; - export const deleteEditorChartArgsSchema = z.object({ chartId: z.string(), }); export const deleteEditorChartResultSchema = z.object({}); - -export const getEditorChartArgsSchema = z.object({ - chardId: z.string(), - workbookId: z.union([z.string(), z.null()]).default(null).optional(), - revId: z.string().optional(), - includePermissions: z.boolean().default(false).optional(), - includeLinks: z.boolean().default(false).optional(), - branch: z.literal(['saved', 'published']).default('published').optional(), -}); - -const editorUsSchema = z.object({ - data: z.union([ - z.object({ - js: z.string(), - url: z.string(), - params: z.string(), - shared: z.string(), - }), - z.object({ - controls: z.string(), - meta: z.string(), - params: z.string(), - prepare: z.string(), - sources: z.string(), - }), - ]), - entryId: z.string(), - scope: z.literal(EntryScope.Widget), - type: z.enum(EDITOR_TYPE), - public: z.boolean(), - isFavorite: z.boolean(), - createdAt: z.string(), - createdBy: z.string(), - updatedAt: z.string(), - updatedBy: z.string(), - revId: z.string(), - savedId: z.string(), - publishedId: z.string(), - meta: z.record(z.string(), z.string()), - links: z.record(z.string(), z.string()).optional(), - key: z.union([z.null(), z.string()]), - workbookId: z.union([z.null(), z.string()]), -}); - -export const getEditorChartResultSchema = editorUsSchema; diff --git a/src/shared/schema/mix/schemas/wizard.ts b/src/shared/schema/mix/schemas/wizard.ts index 386ac0560b..e1af087bdd 100644 --- a/src/shared/schema/mix/schemas/wizard.ts +++ b/src/shared/schema/mix/schemas/wizard.ts @@ -1,58 +1,5 @@ import z from 'zod/v4'; -import {EntryScope, WizardType} from '../../..'; -import {v12ChartsConfigSchema} from '../../../sdk/zod-schemas/wizard-chart-api.schema'; - -export const getWizardChartArgsSchema = z.object({ - chardId: z.string(), - unreleased: z.boolean().default(false).optional(), - revId: z.string().optional(), - includePermissions: z.boolean().default(false).optional(), - includeLinks: z.boolean().default(false).optional(), -}); - -const wizardUsSchema = z.object({ - data: v12ChartsConfigSchema, - entryId: z.string(), - scope: z.literal(EntryScope.Widget), - type: z.enum(WizardType), - public: z.boolean(), - isFavorite: z.boolean(), - createdAt: z.string(), - createdBy: z.string(), - updatedAt: z.string(), - updatedBy: z.string(), - revId: z.string(), - savedId: z.string(), - publishedId: z.string(), - meta: z.record(z.string(), z.string()), - links: z.record(z.string(), z.string()).optional(), - key: z.union([z.null(), z.string()]), - workbookId: z.union([z.null(), z.string()]), -}); - -export const getWizardChartResultSchema = wizardUsSchema; - -export const createWizardChartArgsSchema = z.object({ - entryId: z.string(), - data: v12ChartsConfigSchema, - key: z.string(), - workbookId: z.union([z.string(), z.null()]).optional(), - type: z.enum(WizardType).optional(), - name: z.string(), -}); - -export const createWizardChartResultSchema = wizardUsSchema; - -export const updateWizardChartArgsSchema = z.object({ - entryId: z.string(), - revId: z.string().optional(), - data: v12ChartsConfigSchema, - type: z.enum(WizardType).optional(), -}); - -export const updateWizardChartResultSchema = wizardUsSchema; - export const deleteWizardChartArgsSchema = z.object({ chartId: z.string(), }); diff --git a/src/shared/schema/mix/types/dash.ts b/src/shared/schema/mix/types/dash.ts index 649e267ad9..6a947623e5 100644 --- a/src/shared/schema/mix/types/dash.ts +++ b/src/shared/schema/mix/types/dash.ts @@ -1,9 +1,6 @@ -import type z from 'zod/v4'; - import type {WizardVisualizationId} from '../../../constants'; import type {ChartsStats, DashStats, WorkbookId} from '../../../types'; import type {GetEntriesEntryResponse} from '../../us/types'; -import type {createDashResultSchema, updateDashResultSchema} from '../schemas/dash'; export type CollectDashStatsResponse = { status: string; @@ -54,7 +51,3 @@ export type GetWidgetsDatasetsFieldsArgs = { entriesIds: string[]; workbookId: WorkbookId; }; - -export type UpdateDashResponse = z.infer; - -export type CreateDashResponse = z.infer; diff --git a/src/shared/schema/mix/types/editor.ts b/src/shared/schema/mix/types/editor.ts deleted file mode 100644 index f645cc0149..0000000000 --- a/src/shared/schema/mix/types/editor.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type z from 'zod/v4'; - -import type {getEditorChartResultSchema} from '../schemas/editor'; - -export type GetEditorChartResponse = z.infer; diff --git a/src/shared/sdk/zod-schemas/__tests__/dash-api.schema.test.ts b/src/shared/zod-schemas/__tests__/dash-api.schema.test.ts similarity index 99% rename from src/shared/sdk/zod-schemas/__tests__/dash-api.schema.test.ts rename to src/shared/zod-schemas/__tests__/dash-api.schema.test.ts index fccb1451a3..0db557d101 100644 --- a/src/shared/sdk/zod-schemas/__tests__/dash-api.schema.test.ts +++ b/src/shared/zod-schemas/__tests__/dash-api.schema.test.ts @@ -7,8 +7,8 @@ import { DashTabItemControlSourceType, DashTabItemTitleSizes, DashTabItemType, -} from '../../..'; -import {type DashSchema, dashSchema} from '../dash-api.schema'; +} from '../..'; +import {type DashSchema, dashSchema} from '../dash'; const DASH_DEFAULT_NAMESPACE = 'default'; diff --git a/src/shared/sdk/zod-schemas/dash-api.schema.ts b/src/shared/zod-schemas/dash.ts similarity index 99% rename from src/shared/sdk/zod-schemas/dash-api.schema.ts rename to src/shared/zod-schemas/dash.ts index f1918d1325..d8dbcf77cc 100644 --- a/src/shared/sdk/zod-schemas/dash-api.schema.ts +++ b/src/shared/zod-schemas/dash.ts @@ -9,7 +9,8 @@ import { DashTabItemControlSourceType, DashTabItemTitleSizes, DashTabItemType, -} from '../..'; +} from '..'; + const DASH_DEFAULT_NAMESPACE = 'default'; // Text definition diff --git a/src/shared/sdk/zod-schemas/dataset-api.schema.ts b/src/shared/zod-schemas/dataset.ts similarity index 86% rename from src/shared/sdk/zod-schemas/dataset-api.schema.ts rename to src/shared/zod-schemas/dataset.ts index 8623d4b787..f623bfb9a3 100644 --- a/src/shared/sdk/zod-schemas/dataset-api.schema.ts +++ b/src/shared/zod-schemas/dataset.ts @@ -1,12 +1,12 @@ -import * as z from 'zod/v4'; +import z from 'zod/v4'; -import {ConnectorType} from '../..'; +import {ConnectorType} from '..'; import { DATASET_FIELD_TYPES, DATASET_VALUE_CONSTRAINT_TYPE, DatasetFieldAggregation, DatasetFieldType, -} from '../../types/dataset'; +} from '../types/dataset'; // Basic type schemas const parameterDefaultValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); @@ -71,19 +71,6 @@ const datasetFieldSchema = z.object({ value_constraint: datasetValueConstraintSchema.optional(), }); -// Dataset field error schema -const datasetFieldErrorSchema = z.object({ - guid: z.string(), - title: z.string(), - errors: z.array( - z.object({ - column: z.union([z.number(), z.null()]), - row: z.union([z.number(), z.null()]), - message: z.string(), - }), - ), -}); - // Dataset component error item schema const datasetComponentErrorItemSchema = z.object({ code: z.string(), @@ -260,19 +247,7 @@ const datasetOptionsSchema = z.object({ }), }); -// Dataset API error schema -const datasetApiErrorSchema = z.object({ - datasetId: z.string(), - error: z.object({ - code: z.string().optional(), - message: z.string().optional(), - }), -}); - -// Dataset selection map schema -const datasetSelectionMapSchema = z.record(z.string(), z.literal(true)); - -export const datasetBodySchema = z.object({ +const datasetBodySchema = z.object({ avatar_relations: z.array(datasetAvatarRelationSchema), component_errors: z.object({ items: z.array(datasetComponentErrorSchema), @@ -297,7 +272,7 @@ export const datasetBodySchema = z.object({ }); // Main Dataset schema -export const datasetSchema = z.object({ +const datasetSchema = z.object({ id: z.string(), realName: z.string(), is_favorite: z.boolean(), @@ -321,22 +296,4 @@ export const datasetSchema = z.object({ sources: z.array(datasetSourceSchema), }); -// Export JSON schema generation -export const datasetApiValidationJsonSchema = z.toJSONSchema(datasetSchema); - -// Export the TypeScript type -export type DatasetApiValidationType = z.infer; - -// Export individual schemas for potential reuse -export { - datasetFieldSchema, - datasetSourceSchema, - datasetOptionsSchema, - datasetAvatarRelationSchema, - datasetSourceAvatarSchema, - obligatoryFilterSchema, - datasetComponentErrorSchema, - datasetFieldErrorSchema, - datasetApiErrorSchema, - datasetSelectionMapSchema, -}; +export {datasetBodySchema, datasetOptionsSchema, datasetSchema}; diff --git a/src/shared/sdk/zod-schemas/wizard-chart-api.schema.ts b/src/shared/zod-schemas/wizard.ts similarity index 98% rename from src/shared/sdk/zod-schemas/wizard-chart-api.schema.ts rename to src/shared/zod-schemas/wizard.ts index ea3370d78e..e20f317c68 100644 --- a/src/shared/sdk/zod-schemas/wizard-chart-api.schema.ts +++ b/src/shared/zod-schemas/wizard.ts @@ -7,10 +7,10 @@ import { LabelsPositions, MapCenterMode, ZoomMode, -} from '../..'; -import {WidgetSize} from '../../constants'; -import {MARKUP_TYPE} from '../../types/charts'; -import type {DatasetFieldCalcMode} from '../../types/dataset'; +} from '..'; +import {WidgetSize} from '../constants'; +import {MARKUP_TYPE} from '../types/charts'; +import type {DatasetFieldCalcMode} from '../types/dataset'; import { AxisLabelFormatMode, AxisMode, @@ -18,7 +18,7 @@ import { ChartsConfigVersion, NumberFormatType, NumberFormatUnit, -} from '../../types/wizard'; +} from '../types/wizard'; // Helper type for enum to literal conversion type EnumToLiteral = T extends string From 4dee5d0588b93ab4351551202b3214c8b123abe4 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Mon, 15 Sep 2025 14:08:44 +0300 Subject: [PATCH 33/40] Remove not ready schemas --- .../storage/united-storage/provider.ts | 26 +- src/server/components/sdk/dash.ts | 9 +- .../__tests__/dash-api.schema.test.ts | 989 ------------------ src/shared/zod-schemas/dash.ts | 285 ----- src/shared/zod-schemas/wizard.ts | 496 --------- 5 files changed, 3 insertions(+), 1802 deletions(-) delete mode 100644 src/shared/zod-schemas/__tests__/dash-api.schema.test.ts delete mode 100644 src/shared/zod-schemas/dash.ts delete mode 100644 src/shared/zod-schemas/wizard.ts diff --git a/src/server/components/charts-engine/components/storage/united-storage/provider.ts b/src/server/components/charts-engine/components/storage/united-storage/provider.ts index af31dd0c5e..49854261a6 100644 --- a/src/server/components/charts-engine/components/storage/united-storage/provider.ts +++ b/src/server/components/charts-engine/components/storage/united-storage/provider.ts @@ -27,7 +27,6 @@ import { TRACE_ID_HEADER, US_PUBLIC_API_TOKEN_HEADER, WORKBOOK_ID_HEADER, - mapChartsConfigToLatestVersion, } from '../../../../../../shared'; import {ErrorCode, TIMEOUT_10_SEC} from '../../../../../../shared/constants'; import {createErrorHandler} from '../../error-handler'; @@ -227,7 +226,7 @@ export type ProviderCreateParams = { recursion?: boolean; meta?: Record; includePermissionsInfo?: boolean | string; - workbookId: string | null; + workbookId: string; name: string; mode?: EntryUpdateMode; annotation?: EntryAnnotationArgs; @@ -358,29 +357,6 @@ export class USProvider { }); } - static async retrieveParsedWizardChart( - ctx: AppContext, - props: { - id: string; - storageApiPath?: string; - extraAllowedHeaders?: string[]; - unreleased: boolean | string; - includeLinks?: boolean | string; - includePermissionsInfo?: boolean | string; - revId?: string; - headers: Request['headers']; - workbookId?: WorkbookId; - includeServicePlan?: boolean; - includeTenantFeatures?: boolean; - }, - ) { - const result = await USProvider.retrieveById(ctx, props); - - result.data = mapChartsConfigToLatestVersion(JSON.parse(result.data.shared)) as any; - - return result; - } - static retrieveByKey( ctx: AppContext, { diff --git a/src/server/components/sdk/dash.ts b/src/server/components/sdk/dash.ts index 3b56d02552..30e737c53b 100644 --- a/src/server/components/sdk/dash.ts +++ b/src/server/components/sdk/dash.ts @@ -283,7 +283,6 @@ class Dash { params: EntryReadParams | null, headers: IncomingHttpHeaders, ctx: AppContext, - options?: {forceMigrate?: boolean}, ): Promise { try { const headersWithMetadata = { @@ -298,10 +297,7 @@ class Dash { const isServerMigrationEnabled = Boolean( isEnabledServerFeature(Feature.DashServerMigrationEnable), ); - if ( - (options?.forceMigrate || isServerMigrationEnabled) && - DashSchemeConverter.isUpdateNeeded(result.data) - ) { + if (isServerMigrationEnabled && DashSchemeConverter.isUpdateNeeded(result.data)) { result.data = await Dash.migrate(result.data); } @@ -402,7 +398,6 @@ class Dash { headers: IncomingHttpHeaders, ctx: AppContext, I18n: ServerI18n, - options?: {forceMigrate?: boolean}, ): Promise { try { const usData: typeof data & {skipSyncLinks?: boolean} = {...data}; @@ -411,7 +406,7 @@ class Dash { const needDataSend = !(mode === EntryUpdateMode.Publish && data.revId); if (needDataSend) { if (needSetDefaultData(usData.data)) { - const initialData = await Dash.read(entryId, null, headers, ctx, options); + const initialData = await Dash.read(entryId, null, headers, ctx); usData.data = setDefaultData(I18n, usData.data, initialData.data); } diff --git a/src/shared/zod-schemas/__tests__/dash-api.schema.test.ts b/src/shared/zod-schemas/__tests__/dash-api.schema.test.ts deleted file mode 100644 index 0db557d101..0000000000 --- a/src/shared/zod-schemas/__tests__/dash-api.schema.test.ts +++ /dev/null @@ -1,989 +0,0 @@ -import { - CONTROLS_PLACEMENT_MODE, - DASH_CURRENT_SCHEME_VERSION, - DashLoadPriority, - DashTabConnectionKind, - DashTabItemControlElementType, - DashTabItemControlSourceType, - DashTabItemTitleSizes, - DashTabItemType, -} from '../..'; -import {type DashSchema, dashSchema} from '../dash'; - -const DASH_DEFAULT_NAMESPACE = 'default'; - -describe('dashSchema', () => { - describe('valid configurations', () => { - it('should validate minimal valid dashboard configuration', () => { - const validConfig: DashSchema = { - data: { - tabs: [ - { - id: 'tab1', - title: 'Test Tab', - items: [], - layout: [], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [], - }, - }, - ], - }, - }; - - const result = dashSchema.safeParse(validConfig); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data).toEqual(validConfig); - } - }); - - it('should validate complete dashboard configuration with all optional fields', () => { - const validConfig: DashSchema = { - key: 'dashboard-key-123', - data: { - counter: 1, - salt: 'random-salt-123', - schemeVersion: DASH_CURRENT_SCHEME_VERSION, - tabs: [ - { - id: 'tab1', - title: 'Complete Tab', - items: [ - { - id: 'text1', - namespace: DASH_DEFAULT_NAMESPACE, - type: DashTabItemType.Text, - data: { - text: 'Sample text content', - }, - }, - { - id: 'title1', - namespace: DASH_DEFAULT_NAMESPACE, - type: DashTabItemType.Title, - data: { - text: 'Sample Title', - size: DashTabItemTitleSizes.L, - showInTOC: true, - }, - }, - { - id: 'widget1', - namespace: DASH_DEFAULT_NAMESPACE, - type: DashTabItemType.Widget, - data: { - hideTitle: false, - tabs: [ - { - id: 'widget-tab1', - title: 'Widget Tab', - description: 'Widget description', - chartId: 'chart123', - isDefault: true, - params: {param1: 'value1'}, - autoHeight: true, - }, - ], - }, - }, - ], - layout: [ - { - i: 'text1', - h: 2, - w: 12, - x: 0, - y: 0, - }, - { - i: 'title1', - h: 1, - w: 12, - x: 0, - y: 2, - parent: 'text1', - }, - ], - connections: [ - { - from: 'control1', - to: 'widget1', - kind: DashTabConnectionKind.Ignore, - }, - ], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [['alias1', 'alias2']], - }, - }, - ], - settings: { - autoupdateInterval: 60, - maxConcurrentRequests: 5, - loadPriority: DashLoadPriority.Charts, - silentLoading: true, - dependentSelectors: false, - globalParams: {globalParam: 'value'}, - hideTabs: false, - hideDashTitle: true, - expandTOC: false, - }, - }, - meta: {metaKey: 'metaValue'}, - links: {linkKey: 'linkValue'}, - }; - - const result = dashSchema.safeParse(validConfig); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data).toEqual(validConfig); - } - }); - - it('should validate dashboard with control items', () => { - const validConfig: DashSchema = { - data: { - tabs: [ - { - id: 'tab1', - title: 'Tab with Controls', - items: [ - { - id: 'control1', - namespace: DASH_DEFAULT_NAMESPACE, - type: DashTabItemType.Control, - data: { - title: 'Dataset Control', - sourceType: DashTabItemControlSourceType.Dataset, - source: { - datasetId: 'dataset123', - datasetFieldId: 'field123', - elementType: DashTabItemControlElementType.Select, - required: true, - showHint: true, - showTitle: true, - defaultValue: 'default', - multiselectable: false, - }, - }, - defaults: {defaultParam: 'value'}, - }, - { - id: 'control2', - namespace: DASH_DEFAULT_NAMESPACE, - type: DashTabItemType.Control, - data: { - title: 'Manual Control', - sourceType: DashTabItemControlSourceType.Manual, - source: { - fieldName: 'manualField', - elementType: DashTabItemControlElementType.Date, - required: false, - showHint: false, - showTitle: true, - defaultValue: '2023-01-01', - isRange: true, - acceptableValues: { - from: '2023-01-01', - to: '2023-12-31', - }, - }, - }, - defaults: {}, - }, - { - id: 'control3', - namespace: DASH_DEFAULT_NAMESPACE, - type: DashTabItemType.Control, - data: { - title: 'External Control', - sourceType: DashTabItemControlSourceType.External, - source: { - chartId: 'external-chart-123', - text: 'External control text', - autoHeight: true, - }, - }, - defaults: {}, - }, - ], - layout: [], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [], - }, - }, - ], - }, - }; - - const result = dashSchema.safeParse(validConfig); - expect(result.success).toBe(true); - }); - - it('should validate dashboard with group control', () => { - const validConfig: DashSchema = { - data: { - tabs: [ - { - id: 'tab1', - title: 'Tab with Group Control', - items: [ - { - id: 'groupControl1', - namespace: DASH_DEFAULT_NAMESPACE, - type: DashTabItemType.GroupControl, - data: { - group: [ - { - id: 'groupItem1', - title: 'Group Item 1', - namespace: DASH_DEFAULT_NAMESPACE, - sourceType: DashTabItemControlSourceType.Dataset, - defaults: {param: 'value'}, - placementMode: CONTROLS_PLACEMENT_MODE.AUTO, - width: '50%', - source: { - datasetId: 'dataset123', - datasetFieldId: 'field123', - elementType: - DashTabItemControlElementType.Input, - required: true, - showHint: false, - showTitle: true, - defaultValue: 'input value', - }, - }, - ], - autoHeight: true, - buttonApply: true, - buttonReset: false, - showGroupName: true, - updateControlsOnChange: false, - }, - }, - ], - layout: [], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [], - }, - }, - ], - }, - }; - - const result = dashSchema.safeParse(validConfig); - expect(result.success).toBe(true); - }); - - it('should validate different control element types', () => { - // Test Select control - const selectConfig: DashSchema = { - data: { - tabs: [ - { - id: 'tab1', - title: 'Test Tab', - items: [ - { - id: 'control-select', - namespace: DASH_DEFAULT_NAMESPACE, - type: DashTabItemType.Control, - data: { - title: 'Select Control', - sourceType: DashTabItemControlSourceType.Manual, - source: { - fieldName: 'selectField', - elementType: DashTabItemControlElementType.Select, - required: false, - showHint: true, - showTitle: true, - defaultValue: ['option1', 'option2'], - multiselectable: true, - }, - }, - defaults: {}, - }, - ], - layout: [], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [], - }, - }, - ], - }, - }; - - // Test Date control - const dateConfig: DashSchema = { - data: { - tabs: [ - { - id: 'tab1', - title: 'Test Tab', - items: [ - { - id: 'control-date', - namespace: DASH_DEFAULT_NAMESPACE, - type: DashTabItemType.Control, - data: { - title: 'Date Control', - sourceType: DashTabItemControlSourceType.Manual, - source: { - fieldName: 'dateField', - elementType: DashTabItemControlElementType.Date, - required: false, - showHint: true, - showTitle: true, - defaultValue: '2023-01-01', - isRange: false, - }, - }, - defaults: {}, - }, - ], - layout: [], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [], - }, - }, - ], - }, - }; - - // Test Input control - const inputConfig: DashSchema = { - data: { - tabs: [ - { - id: 'tab1', - title: 'Test Tab', - items: [ - { - id: 'control-input', - namespace: DASH_DEFAULT_NAMESPACE, - type: DashTabItemType.Control, - data: { - title: 'Input Control', - sourceType: DashTabItemControlSourceType.Manual, - source: { - fieldName: 'inputField', - elementType: DashTabItemControlElementType.Input, - required: false, - showHint: true, - showTitle: true, - defaultValue: 'input text', - }, - }, - defaults: {}, - }, - ], - layout: [], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [], - }, - }, - ], - }, - }; - - // Test Checkbox control - const checkboxConfig: DashSchema = { - data: { - tabs: [ - { - id: 'tab1', - title: 'Test Tab', - items: [ - { - id: 'control-checkbox', - namespace: DASH_DEFAULT_NAMESPACE, - type: DashTabItemType.Control, - data: { - title: 'Checkbox Control', - sourceType: DashTabItemControlSourceType.Manual, - source: { - fieldName: 'checkboxField', - elementType: DashTabItemControlElementType.Checkbox, - required: false, - showHint: true, - showTitle: true, - defaultValue: 'true', - }, - }, - defaults: {}, - }, - ], - layout: [], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [], - }, - }, - ], - }, - }; - - const configs = [selectConfig, dateConfig, inputConfig, checkboxConfig]; - configs.forEach((config) => { - const result = dashSchema.safeParse(config); - expect(result.success).toBe(true); - }); - }); - - it('should validate different title sizes', () => { - const titleSizes = Object.values(DashTabItemTitleSizes); - - titleSizes.forEach((size, index) => { - const config: DashSchema = { - data: { - tabs: [ - { - id: 'tab1', - title: 'Test Tab', - items: [ - { - id: `title${index}`, - namespace: DASH_DEFAULT_NAMESPACE, - type: DashTabItemType.Title, - data: { - text: `Title ${size}`, - size: size, - }, - }, - ], - layout: [], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [], - }, - }, - ], - }, - }; - - const result = dashSchema.safeParse(config); - expect(result.success).toBe(true); - }); - }); - }); - - describe('invalid configurations', () => { - it('should reject configuration without required data field', () => { - const invalidConfig = { - key: 'test-key', - }; - - const result = dashSchema.safeParse(invalidConfig); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues).toContainEqual( - expect.objectContaining({ - code: 'invalid_type', - expected: 'object', - message: 'Invalid input: expected object, received undefined', - path: ['data'], - }), - ); - } - }); - - it('should reject configuration without tabs', () => { - const invalidConfig = { - data: { - settings: {}, - }, - }; - - const result = dashSchema.safeParse(invalidConfig); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues).toContainEqual( - expect.objectContaining({ - code: 'invalid_type', - path: ['data', 'tabs'], - message: 'Invalid input: expected array, received undefined', - }), - ); - } - }); - - it('should reject tab with empty id', () => { - const invalidConfig = { - data: { - tabs: [ - { - id: '', - title: 'Test Tab', - items: [], - layout: [], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [], - }, - }, - ], - }, - }; - - const result = dashSchema.safeParse(invalidConfig); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues).toContainEqual( - expect.objectContaining({ - code: 'too_small', - path: ['data', 'tabs', 0, 'id'], - }), - ); - } - }); - - it('should reject tab with empty title', () => { - const invalidConfig = { - data: { - tabs: [ - { - id: 'tab1', - title: '', - items: [], - layout: [], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [], - }, - }, - ], - }, - }; - - const result = dashSchema.safeParse(invalidConfig); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues).toContainEqual( - expect.objectContaining({ - code: 'too_small', - path: ['data', 'tabs', 0, 'title'], - }), - ); - } - }); - - it('should reject item with invalid namespace', () => { - const invalidConfig = { - data: { - tabs: [ - { - id: 'tab1', - title: 'Test Tab', - items: [ - { - id: 'text1', - namespace: 'invalid-namespace', - type: DashTabItemType.Text, - data: { - text: 'Sample text', - }, - }, - ], - layout: [], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [], - }, - }, - ], - }, - }; - - const result = dashSchema.safeParse(invalidConfig); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues).toContainEqual( - expect.objectContaining({ - code: 'invalid_value', - path: ['data', 'tabs', 0, 'items', 0, 'namespace'], - }), - ); - } - }); - - it('should reject control with invalid element type', () => { - const invalidConfig = { - data: { - tabs: [ - { - id: 'tab1', - title: 'Test Tab', - items: [ - { - id: 'control1', - namespace: DASH_DEFAULT_NAMESPACE, - type: DashTabItemType.Control, - data: { - title: 'Invalid Control', - sourceType: DashTabItemControlSourceType.Manual, - source: { - fieldName: 'field1', - elementType: 'invalid-type', - required: false, - showHint: false, - showTitle: true, - }, - }, - defaults: {}, - }, - ], - layout: [], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [], - }, - }, - ], - }, - }; - - const result = dashSchema.safeParse(invalidConfig); - expect(result.success).toBe(false); - }); - - it('should reject widget tab with empty chartId', () => { - const invalidConfig = { - data: { - tabs: [ - { - id: 'tab1', - title: 'Test Tab', - items: [ - { - id: 'widget1', - namespace: DASH_DEFAULT_NAMESPACE, - type: DashTabItemType.Widget, - data: { - hideTitle: false, - tabs: [ - { - id: 'widget-tab1', - title: 'Widget Tab', - chartId: '', - }, - ], - }, - }, - ], - layout: [], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [], - }, - }, - ], - }, - }; - - const result = dashSchema.safeParse(invalidConfig); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues).toContainEqual( - expect.objectContaining({ - code: 'too_small', - path: ['data', 'tabs', 0, 'items', 0, 'data', 'tabs', 0, 'chartId'], - }), - ); - } - }); - - it('should reject invalid scheme version', () => { - const invalidConfig = { - data: { - schemeVersion: DASH_CURRENT_SCHEME_VERSION + 1, - tabs: [ - { - id: 'tab1', - title: 'Test Tab', - items: [], - layout: [], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [], - }, - }, - ], - }, - }; - - const result = dashSchema.safeParse(invalidConfig); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues).toContainEqual( - expect.objectContaining({ - code: 'too_big', - path: ['data', 'schemeVersion'], - }), - ); - } - }); - - it('should reject invalid autoupdate interval', () => { - const invalidConfig = { - data: { - tabs: [], - settings: { - autoupdateInterval: 15, // less than minimum 30 - }, - }, - }; - - const result = dashSchema.safeParse(invalidConfig); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues).toContainEqual( - expect.objectContaining({ - code: 'invalid_union', - path: ['data', 'settings', 'autoupdateInterval'], - }), - ); - } - }); - - it('should reject invalid maxConcurrentRequests', () => { - const invalidConfig = { - data: { - tabs: [], - settings: { - maxConcurrentRequests: 0, // less than minimum 1 - }, - }, - }; - - const result = dashSchema.safeParse(invalidConfig); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues).toContainEqual( - expect.objectContaining({ - code: 'invalid_union', - path: ['data', 'settings', 'maxConcurrentRequests'], - }), - ); - } - }); - - it('should accept layout item with negative dimensions (schema allows this)', () => { - const validConfig = { - data: { - tabs: [ - { - id: 'tab1', - title: 'Test Tab', - items: [], - layout: [ - { - i: 'item1', - h: -1, - w: 12, - x: 0, - y: 0, - }, - ], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [], - }, - }, - ], - }, - }; - - const result = dashSchema.safeParse(validConfig); - expect(result.success).toBe(true); - }); - - it('should reject connection with empty from/to fields', () => { - const invalidConfig = { - data: { - tabs: [ - { - id: 'tab1', - title: 'Test Tab', - items: [], - layout: [], - connections: [ - { - from: '', - to: 'widget1', - kind: DashTabConnectionKind.Ignore, - }, - ], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [], - }, - }, - ], - }, - }; - - const result = dashSchema.safeParse(invalidConfig); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues).toContainEqual( - expect.objectContaining({ - code: 'too_small', - path: ['data', 'tabs', 0, 'connections', 0, 'from'], - }), - ); - } - }); - - it('should reject aliases with invalid structure', () => { - const invalidConfig = { - data: { - tabs: [ - { - id: 'tab1', - title: 'Test Tab', - items: [], - layout: [], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [['single-item']], // should have at least 2 items - }, - }, - ], - }, - }; - - const result = dashSchema.safeParse(invalidConfig); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues).toContainEqual( - expect.objectContaining({ - code: 'too_small', - path: ['data', 'tabs', 0, 'aliases', DASH_DEFAULT_NAMESPACE, 0], - }), - ); - } - }); - }); - - describe('edge cases', () => { - it('should handle null values in settings', () => { - const config = { - data: { - tabs: [], - settings: { - autoupdateInterval: null, - maxConcurrentRequests: null, - }, - }, - }; - - const result = dashSchema.safeParse(config); - expect(result.success).toBe(true); - }); - - it('should handle string autoupdate interval', () => { - const config = { - data: { - tabs: [], - settings: { - autoupdateInterval: '60', - }, - }, - }; - - const result = dashSchema.safeParse(config); - expect(result.success).toBe(true); - }); - - it('should handle empty key', () => { - const config = { - key: '', - data: { - tabs: [], - }, - }; - - const result = dashSchema.safeParse(config); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues).toContainEqual( - expect.objectContaining({ - code: 'too_small', - path: ['key'], - }), - ); - } - }); - - it('should handle complex nested structures', () => { - const config: DashSchema = { - data: { - tabs: [ - { - id: 'complex-tab', - title: 'Complex Tab', - items: [ - { - id: 'complex-widget', - namespace: DASH_DEFAULT_NAMESPACE, - type: DashTabItemType.Widget, - data: { - hideTitle: false, - tabs: [ - { - id: 'nested-tab-1', - title: 'Nested Tab 1', - description: 'First nested tab', - chartId: 'chart-1', - isDefault: true, - params: { - complexParam: { - nested: { - value: 'deep-value', - array: [1, 2, 3], - }, - }, - }, - autoHeight: false, - }, - { - id: 'nested-tab-2', - title: 'Nested Tab 2', - chartId: 'chart-2', - params: {}, - }, - ], - }, - }, - ], - layout: [ - { - i: 'complex-widget', - h: 10, - w: 24, - x: 6, - y: 5, - }, - ], - connections: [], - aliases: { - [DASH_DEFAULT_NAMESPACE]: [ - ['alias-group-1', 'alias-group-2'], - ['another-alias-1', 'another-alias-2'], - ], - }, - }, - ], - }, - }; - - const result = dashSchema.safeParse(config); - expect(result.success).toBe(true); - }); - }); -}); diff --git a/src/shared/zod-schemas/dash.ts b/src/shared/zod-schemas/dash.ts deleted file mode 100644 index d8dbcf77cc..0000000000 --- a/src/shared/zod-schemas/dash.ts +++ /dev/null @@ -1,285 +0,0 @@ -import * as z from 'zod/v4'; - -import { - CONTROLS_PLACEMENT_MODE, - DASH_CURRENT_SCHEME_VERSION, - DashLoadPriority, - DashTabConnectionKind, - DashTabItemControlElementType, - DashTabItemControlSourceType, - DashTabItemTitleSizes, - DashTabItemType, -} from '..'; - -const DASH_DEFAULT_NAMESPACE = 'default'; - -// Text definition -const textSchema = z.object({ - text: z.string(), -}); - -// Title definition -const titleSchema = z.object({ - text: z.string(), - size: z.enum(DashTabItemTitleSizes), - showInTOC: z.boolean().optional(), -}); - -// Widget definition -const widgetSchema = z.object({ - hideTitle: z.boolean().optional(), - tabs: z.array( - z.object({ - id: z.string().min(1), - title: z.string().min(1), - description: z.string().optional(), - chartId: z.string().min(1).optional(), - isDefault: z.boolean().optional(), - params: z.record(z.any(), z.any()).optional(), - autoHeight: z.boolean().optional(), - }), - ), -}); - -// Control element type definition -const controlElementTypeSchema = z - .object({ - required: z.boolean().optional(), - showHint: z.boolean().optional(), - showTitle: z.boolean().optional(), - elementType: z.enum(DashTabItemControlElementType), - }) - .and( - z.discriminatedUnion('elementType', [ - z.object({ - elementType: z.literal(DashTabItemControlElementType.Select), - defaultValue: z.union([z.string(), z.array(z.string()), z.null()]).optional(), - multiselectable: z.boolean().optional(), - }), - z.object({ - elementType: z.literal(DashTabItemControlElementType.Date), - defaultValue: z.string().optional(), - isRange: z.boolean().optional(), - }), - z.object({ - elementType: z.literal(DashTabItemControlElementType.Input), - defaultValue: z.string().optional(), - }), - z.object({ - elementType: z.literal(DashTabItemControlElementType.Checkbox), - defaultValue: z.string().optional(), - }), - ]), - ); - -// Control source dataset definition -const controlSourceDatasetSchema = controlElementTypeSchema.and( - z.object({ - datasetId: z.string().min(1), - datasetFieldId: z.string().min(1), - }), -); - -// Control source manual definition -const controlSourceManualSchema = controlElementTypeSchema.and( - z.object({ - fieldName: z.string().min(1), - acceptableValues: z - .union([ - // elementType: select - z.array( - z.object({ - value: z.string(), - title: z.string(), - }), - ), - // elementType: date - z.object({ - from: z.string().optional(), - to: z.string().optional(), - }), - ]) - .optional(), - }), -); - -// Control source external definition -const controlSourceExternalSchema = z.object({ - chartId: z.string().min(1), - text: z.string().optional(), - autoHeight: z.boolean().optional(), -}); - -// Control definition -const controlSchema = z - .object({ - title: z.string().min(1), - sourceType: z.enum(DashTabItemControlSourceType), - }) - .and( - z.discriminatedUnion('sourceType', [ - z.object({ - sourceType: z.literal(DashTabItemControlSourceType.Dataset), - source: controlSourceDatasetSchema, - }), - z.object({ - sourceType: z.literal(DashTabItemControlSourceType.Manual), - source: controlSourceManualSchema, - }), - z.object({ - sourceType: z.literal(DashTabItemControlSourceType.External), - source: controlSourceExternalSchema, - }), - ]), - ); - -// Group control items definition -const groupControlItemsSchema = z - .object({ - id: z.string().min(1), - title: z.string().min(1), - namespace: z.literal(DASH_DEFAULT_NAMESPACE), - sourceType: z.union([ - z.literal(DashTabItemControlSourceType.Dataset), - z.literal(DashTabItemControlSourceType.Manual), - ]), - defaults: z.record(z.any(), z.any()), - placementMode: z.enum(CONTROLS_PLACEMENT_MODE).optional(), - width: z.string().optional(), - }) - .and( - z.discriminatedUnion('sourceType', [ - z.object({ - sourceType: z.literal(DashTabItemControlSourceType.Dataset), - source: controlSourceDatasetSchema, - }), - z.object({ - sourceType: z.literal(DashTabItemControlSourceType.Manual), - source: controlSourceManualSchema, - }), - ]), - ); - -// Group control definition -const groupControlSchema = z.object({ - group: z.array(groupControlItemsSchema), - autoHeight: z.boolean().optional(), - buttonApply: z.boolean().optional(), - buttonReset: z.boolean().optional(), - showGroupName: z.boolean().optional(), - updateControlsOnChange: z.boolean().optional(), -}); - -// Layout item definition -const layoutItemSchema = z.object({ - i: z.string().min(1), - h: z.number(), - w: z.number(), - x: z.number(), - y: z.number(), - parent: z.string().optional(), -}); - -// Connection definition -const connectionSchema = z.object({ - from: z.string().min(1), - to: z.string().min(1), - kind: z.enum(DashTabConnectionKind), -}); - -// Tab item definition -const tabItemSchema = z - .object({ - id: z.string().min(1), - namespace: z.literal(DASH_DEFAULT_NAMESPACE), - type: z.enum(DashTabItemType), - }) - .and( - z.discriminatedUnion('type', [ - z.object({ - type: z.literal(DashTabItemType.Text), - data: textSchema, - }), - z.object({ - type: z.literal(DashTabItemType.Title), - data: titleSchema, - }), - z.object({ - type: z.literal(DashTabItemType.Widget), - data: widgetSchema, - }), - z.object({ - type: z.literal(DashTabItemType.Control), - data: controlSchema, - defaults: z.record(z.any(), z.any()), - }), - z.object({ - type: z.literal(DashTabItemType.GroupControl), - data: groupControlSchema, - }), - ]), - ); - -// Alias definition -const aliasRecordSchema = z.array(z.string().min(1)).max(2).min(2); - -// Tab definition -const tabSchema = z - .object({ - id: z.string().min(1), - title: z.string().min(1), - items: z.array(tabItemSchema), - layout: z.array(layoutItemSchema), - connections: z.array(connectionSchema), - aliases: z - .object({ - [DASH_DEFAULT_NAMESPACE]: z.array(aliasRecordSchema).optional(), - }) - .strict(), - }) - .strict(); - -// Settings definition -const settingsSchema = z.object({ - autoupdateInterval: z.union([z.number().min(30), z.string(), z.null()]).optional(), - maxConcurrentRequests: z.union([z.number().min(1), z.null()]).optional(), - loadPriority: z.enum(DashLoadPriority).optional(), - silentLoading: z.boolean().optional(), - dependentSelectors: z.boolean().optional(), - globalParams: z.record(z.any(), z.any()).optional(), - hideTabs: z.boolean().optional(), - hideDashTitle: z.boolean().optional(), - expandTOC: z.boolean().optional(), - assistantEnabled: z.boolean().optional(), -}); - -// Data definition -const dataSchema = z.object({ - counter: z.number().int().min(1).optional(), - salt: z.string().min(1).optional(), - schemeVersion: z - .number() - .int() - .min(1) - .max(DASH_CURRENT_SCHEME_VERSION) - .default(DASH_CURRENT_SCHEME_VERSION) - .optional(), - tabs: z.array(tabSchema), - settings: settingsSchema.optional(), - supportDescription: z.string().optional(), - accessDescription: z.string().optional(), -}); - -// Main dashboard API validation schema -export const dashSchema = z.object({ - key: z.string().min(1).optional(), - workbookId: z.union([z.null(), z.string()]).optional(), - data: dataSchema, - meta: z.record(z.any(), z.any()).optional(), - links: z.record(z.string(), z.string()).optional(), -}); - -export const dashApiValidationJsonSchema = z.toJSONSchema(dashSchema); - -// Export the type for TypeScript usage -export type DashSchema = z.infer; diff --git a/src/shared/zod-schemas/wizard.ts b/src/shared/zod-schemas/wizard.ts deleted file mode 100644 index e20f317c68..0000000000 --- a/src/shared/zod-schemas/wizard.ts +++ /dev/null @@ -1,496 +0,0 @@ -import * as z from 'zod/v4'; - -import { - ColorMode, - GradientNullModes, - IndicatorTitleMode, - LabelsPositions, - MapCenterMode, - ZoomMode, -} from '..'; -import {WidgetSize} from '../constants'; -import {MARKUP_TYPE} from '../types/charts'; -import type {DatasetFieldCalcMode} from '../types/dataset'; -import { - AxisLabelFormatMode, - AxisMode, - AxisNullsMode, - ChartsConfigVersion, - NumberFormatType, - NumberFormatUnit, -} from '../types/wizard'; - -// Helper type for enum to literal conversion -type EnumToLiteral = T extends string - ? `${T}` - : `${T}` extends `${infer N extends number}` - ? N - : never; - -type ValueOf = T[keyof T]; - -// Constants for enum arrays - base arrays -const showHideValuesList = ['show', 'hide'] as const; -const onOffValuesList = ['on', 'off'] as const; -const autoManualValuesList = ['auto', 'manual'] as const; -const yesNoValuesList = ['yes', 'no'] as const; - -// Constants for literal values -const SCALE_VALUE_LITERAL = '0-max'; -const LOGARITHMIC_TYPE_LITERAL = 'logarithmic'; -const MANUAL_GRID_STEP_LITERAL = 'manual'; -const DATALENS_TYPE_LITERAL = 'datalens'; - -// Constants for enum arrays - specific arrays -const DatasetFieldCalcModeList: DatasetFieldCalcMode[] = ['formula', 'direct', 'parameter']; -const titleModeList = showHideValuesList; -const legendModeList = showHideValuesList; -const tooltipModeList = showHideValuesList; -const onOffList = onOffValuesList; -const groupingList = ['disabled', 'off'] as const; -const scaleList = autoManualValuesList; -const titleList = ['auto', 'manual', 'off'] as const; -const gridList = onOffValuesList; -const hideLabelsLis = yesNoValuesList; -const labelsViewList = ['horizontal', 'vertical', 'angle'] as const; -const holidaysList = onOffValuesList; -const axisVisibilityList = showHideValuesList; -const colorFieldTitleList = onOffValuesList; -const v12UpdateActionsList = [ - 'add_field', - 'add', - 'update_field', - 'update', - 'delete', - 'delete_field', -] as const; - -// Basic schemas -const parameterDefaultValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); -const datasetFieldCalcModeSchema = z.enum(DatasetFieldCalcModeList); - -// Enum schemas using Object.values for regular enums -const colorModeSchema = z.enum(Object.values(ColorMode) as EnumToLiteral[]); -const gradientNullModeSchema = z.enum( - Object.values(GradientNullModes) as ValueOf[], -); -const indicatorTitleModeSchema = z.enum([ - IndicatorTitleMode.ByField, - IndicatorTitleMode.Hide, - IndicatorTitleMode.Manual, -]); -const labelsPositionsSchema = z.enum( - Object.values(LabelsPositions) as EnumToLiteral[], -); -const markupTypeSchema = z.enum( - Object.values(MARKUP_TYPE) as [ - EnumToLiteral<(typeof MARKUP_TYPE)[keyof typeof MARKUP_TYPE]>, - ...EnumToLiteral<(typeof MARKUP_TYPE)[keyof typeof MARKUP_TYPE]>[], - ], -); -const widgetSizeTypeSchema = z.enum( - Object.values(WidgetSize) as [ - EnumToLiteral<(typeof WidgetSize)[keyof typeof WidgetSize]>, - ...EnumToLiteral<(typeof WidgetSize)[keyof typeof WidgetSize]>[], - ], -); - -// Const enum schemas using direct values -const axisLabelFormatModeSchema = z.enum([AxisLabelFormatMode.Auto, AxisLabelFormatMode.ByField]); -const axisModeSchema = z.enum([AxisMode.Discrete, AxisMode.Continuous]); -const axisNullsModeSchema = z.enum([ - AxisNullsMode.Ignore, - AxisNullsMode.Connect, - AxisNullsMode.AsZero, - AxisNullsMode.UsePrevious, -]); -const numberFormatTypeSchema = z.enum([NumberFormatType.Number, NumberFormatType.Percent]); -const numberFormatUnitSchema = z.enum([ - NumberFormatUnit.Auto, - NumberFormatUnit.B, - NumberFormatUnit.K, - NumberFormatUnit.M, - NumberFormatUnit.T, -]); - -// Simple string enums for types that are not available as runtime values -const mapCenterModesSchema = z.enum([MapCenterMode.Auto, MapCenterMode.Manual]); -const zoomModesSchema = z.enum([ZoomMode.Auto, ZoomMode.Manual]); - -// V12ClientOnlyFields schema -const v12ClientOnlyFieldsSchema = z.object({ - fakeTitle: z.string().optional(), - originalTitle: z.string().optional(), - markupType: markupTypeSchema.optional(), -}); - -// V12Formatting schema -const v12FormattingSchema = z.object({ - format: numberFormatTypeSchema.optional(), - showRankDelimiter: z.boolean().optional(), - prefix: z.string().optional(), - postfix: z.string().optional(), - unit: numberFormatUnitSchema.optional(), - precision: z.number().optional(), - labelMode: z.string().optional(), -}); - -// V12NavigatorSettings schema -const v12NavigatorSettingsSchema = z.object({ - navigatorMode: z.string(), - isNavigatorAvailable: z.boolean(), - selectedLines: z.array(z.string()), - linesMode: z.string(), - periodSettings: z.object({ - type: z.string(), - value: z.string(), - period: z.string(), - }), -}); - -// V12CommonSharedExtraSettings schema -const v12CommonSharedExtraSettingsSchema = z.object({ - title: z.string().optional(), - titleMode: z.enum(titleModeList).optional(), - indicatorTitleMode: indicatorTitleModeSchema.optional(), - legendMode: z.enum(legendModeList).optional(), - metricFontSize: z.string().optional(), - metricFontColor: z.string().optional(), - tooltip: z.enum(tooltipModeList).optional(), - tooltipSum: z.enum(onOffList).optional(), - limit: z.number().optional(), - pagination: z.enum(onOffList).optional(), - navigatorMode: z.string().optional(), - navigatorSeriesName: z.string().optional(), - totals: z.enum(onOffList).optional(), - pivotFallback: z.enum(onOffList).optional(), - pivotInlineSort: z.enum(onOffList).optional(), - stacking: z.enum(onOffList).optional(), - overlap: z.enum(onOffList).optional(), - feed: z.string().optional(), - navigatorSettings: v12NavigatorSettingsSchema.optional(), - enableGPTInsights: z.boolean().optional(), - labelsPosition: labelsPositionsSchema.optional(), - pinnedColumns: z.number().optional(), - size: widgetSizeTypeSchema.optional(), - zoomMode: zoomModesSchema.optional(), - zoomValue: z.number().nullable().optional(), - mapCenterMode: mapCenterModesSchema.optional(), - mapCenterValue: z.string().nullable().optional(), - preserveWhiteSpace: z.boolean().optional(), -}); - -// V12Filter schema -const v12FilterSchema = z - .object({ - guid: z.string(), - datasetId: z.string(), - disabled: z.string().optional(), - filter: z.object({ - operation: z.object({ - code: z.string(), - }), - value: z.union([z.string(), z.array(z.string())]).optional(), - }), - type: z.string(), - title: z.string(), - calc_mode: datasetFieldCalcModeSchema, - }) - .merge(v12ClientOnlyFieldsSchema); - -// V12Sort schema -const v12SortSchema = z - .object({ - guid: z.string(), - title: z.string(), - source: z.string().optional(), - datasetId: z.string(), - direction: z.string(), - data_type: z.string(), - format: z.string().optional(), - type: z.string(), - default_value: parameterDefaultValueSchema.optional(), - }) - .merge(v12ClientOnlyFieldsSchema); - -// V12LinkField schema -const v12LinkFieldSchema = z.object({ - field: z.object({ - title: z.string(), - guid: z.string(), - }), - dataset: z.object({ - id: z.string(), - realName: z.string(), - }), -}); - -// V12Link schema -const v12LinkSchema = z.object({ - id: z.string(), - fields: z.record(z.string(), v12LinkFieldSchema), -}); - -// V12PlaceholderSettings schema -const v12PlaceholderSettingsSchema = z.object({ - groupping: z.enum(groupingList).optional(), - autoscale: z.boolean().optional(), - scale: z.enum(scaleList).optional(), - scaleValue: z - .union([z.literal(SCALE_VALUE_LITERAL), z.tuple([z.string(), z.string()])]) - .optional(), - title: z.enum(titleList).optional(), - titleValue: z.string().optional(), - type: z.literal(LOGARITHMIC_TYPE_LITERAL).optional(), - grid: z.enum(gridList).optional(), - gridStep: z.literal(MANUAL_GRID_STEP_LITERAL).optional(), - gridStepValue: z.number().optional(), - hideLabels: z.enum(hideLabelsLis).optional(), - labelsView: z.enum(labelsViewList).optional(), - nulls: axisNullsModeSchema.optional(), - holidays: z.enum(holidaysList).optional(), - axisFormatMode: axisLabelFormatModeSchema.optional(), - axisModeMap: z.record(z.string(), axisModeSchema).optional(), - disableAxisMode: z.boolean().optional(), - axisVisibility: z.enum(axisVisibilityList).optional(), -}); - -// Forward declaration for recursive types -const v12FieldSchemaInner = z.object({ - ...v12ClientOnlyFieldsSchema.shape, - data_type: z.string(), - type: z.string(), - title: z.string(), - guid: z.string(), - formatting: v12FormattingSchema.optional(), - format: z.string().optional(), - datasetId: z.string(), - source: z.string().optional(), - datasetName: z.string().optional(), - hideLabelMode: z.string().optional(), - calc_mode: datasetFieldCalcModeSchema, - default_value: parameterDefaultValueSchema.optional(), - barsSettings: z.any().optional(), // TableBarsSettings - subTotalsSettings: z.any().optional(), // TableSubTotalsSettings - backgroundSettings: z.any().optional(), // TableFieldBackgroundSettings - columnSettings: z.any().optional(), // ColumnSettings - hintSettings: z.any().optional(), // HintSettings -}); - -const v12FieldSchema = z.object({ - ...v12FieldSchemaInner.shape, - fields: z.array(v12FieldSchemaInner).optional(), -}); - -// V12Placeholder schema -const v12PlaceholderSchema = z.object({ - id: z.string(), - settings: v12PlaceholderSettingsSchema.optional(), - required: z.boolean().optional(), - capacity: z.number().optional(), - items: z.array(v12FieldSchema), -}); - -// V12Color schema -const v12ColorSchema = z - .object({ - datasetId: z.string(), - guid: z.string(), - title: z.string(), - type: z.string(), - data_type: z.string(), - formatting: v12FormattingSchema.optional(), - calc_mode: datasetFieldCalcModeSchema, - }) - .merge(v12ClientOnlyFieldsSchema); - -// V12Shape schema -const v12ShapeSchema = z - .object({ - datasetId: z.string(), - guid: z.string(), - title: z.string(), - originalTitle: z.string().optional(), - type: z.string(), - data_type: z.string(), - calc_mode: datasetFieldCalcModeSchema, - }) - .merge(v12ClientOnlyFieldsSchema); - -// V12Tooltip schema -const v12TooltipSchema = z - .object({ - datasetId: z.string(), - guid: z.string(), - title: z.string(), - formatting: v12FormattingSchema.optional(), - data_type: z.string(), - calc_mode: datasetFieldCalcModeSchema, - }) - .merge(v12ClientOnlyFieldsSchema); - -// V12Label schema -const v12LabelSchema = z.object({ - datasetId: z.string(), - type: z.string(), - title: z.string(), - guid: z.string(), - formatting: v12FormattingSchema.optional(), - format: z.string().optional(), - data_type: z.string(), - calc_mode: datasetFieldCalcModeSchema, -}); - -// V12HierarchyField schema -const v12HierarchyFieldSchema = z.object({ - data_type: z.string(), - fields: z.array(v12FieldSchema), - type: z.string(), -}); - -// V12PointSizeConfig schema -const v12PointSizeConfigSchema = z.object({ - radius: z.number(), - minRadius: z.number(), - maxRadius: z.number(), -}); - -// V12ColorsConfig schema -const v12ColorsConfigSchema = z.object({ - thresholdsMode: z.string().optional(), - leftThreshold: z.string().optional(), - middleThreshold: z.string().optional(), - rightThreshold: z.string().optional(), - gradientPalette: z.string().optional(), - gradientMode: z.string().optional(), - polygonBorders: z.string().optional(), - reversed: z.boolean().optional(), - fieldGuid: z.string().optional(), - mountedColors: z.record(z.string(), z.string()).optional(), - coloredByMeasure: z.boolean().optional(), - palette: z.string().optional(), - colorMode: colorModeSchema.optional(), - nullMode: gradientNullModeSchema.optional(), -}); - -// V12ShapesConfig schema -const v12ShapesConfigSchema = z.object({ - mountedShapes: z.record(z.string(), z.string()).optional(), - fieldGuid: z.string().optional(), -}); - -// V12TooltipConfig schema -const v12TooltipConfigSchema = z.object({ - color: z.enum(colorFieldTitleList).optional(), - fieldTitle: z.enum(colorFieldTitleList).optional(), -}); - -// V12LayerSettings schema -const v12LayerSettingsSchema = z.object({ - id: z.string(), - name: z.string(), - type: z.string(), - alpha: z.number(), - valid: z.boolean(), -}); - -// V12CommonPlaceholders schema -const v12CommonPlaceholdersSchema = z.object({ - colors: z.array(v12ColorSchema), - labels: z.array(v12LabelSchema), - tooltips: z.array(v12TooltipSchema), - filters: z.array(v12FilterSchema), - sort: z.array(v12SortSchema), - shapes: z.array(v12ShapeSchema).optional(), - colorsConfig: v12ColorsConfigSchema.optional(), - geopointsConfig: v12PointSizeConfigSchema.optional(), - shapesConfig: v12ShapesConfigSchema.optional(), - tooltipConfig: v12TooltipConfigSchema.optional(), -}); - -// V12Layer schema -const v12LayerSchema = z.object({ - id: z.string(), - commonPlaceholders: v12CommonPlaceholdersSchema, - layerSettings: v12LayerSettingsSchema, - placeholders: z.array(v12PlaceholderSchema), -}); - -// V12Visualization schema -const v12VisualizationSchema = z.object({ - id: z.string(), - highchartsId: z.string().optional(), - selectedLayerId: z.string().optional(), - layers: z.array(v12LayerSchema).optional(), - placeholders: z.array(v12PlaceholderSchema), -}); - -// V12Update schema -const v12UpdateSchema = z.object({ - action: z.enum(v12UpdateActionsList), - field: z.any(), - debug_info: z.string().optional(), -}); - -// V12ChartsConfigDatasetField schema -const v12ChartsConfigDatasetFieldSchema = z.object({ - guid: z.string(), - title: z.string(), - calc_mode: datasetFieldCalcModeSchema.optional(), -}); - -// Main V12ChartsConfig schema -export const v12ChartsConfigSchema = z.object({ - title: z.string().optional(), - colors: z.array(v12ColorSchema), - colorsConfig: v12ColorsConfigSchema.optional(), - extraSettings: v12CommonSharedExtraSettingsSchema.optional(), - filters: z.array(v12FilterSchema), - geopointsConfig: v12PointSizeConfigSchema.optional(), - hierarchies: z.array(v12HierarchyFieldSchema), - labels: z.array(v12LabelSchema), - links: z.array(v12LinkSchema), - sort: z.array(v12SortSchema), - tooltips: z.array(v12TooltipSchema), - tooltipConfig: v12TooltipConfigSchema.optional(), - type: z.literal(DATALENS_TYPE_LITERAL), - updates: z.array(v12UpdateSchema), - visualization: v12VisualizationSchema, - shapes: z.array(v12ShapeSchema), - shapesConfig: v12ShapesConfigSchema.optional(), - version: z.literal(ChartsConfigVersion.V12), - datasetsIds: z.array(z.string()), - datasetsPartialFields: z.array(z.array(v12ChartsConfigDatasetFieldSchema)), - segments: z.array(v12FieldSchema), - chartType: z.string().optional(), -}); - -// Export JSON schema generation -export const chartApiValidationJsonSchema = z.toJSONSchema(v12ChartsConfigSchema); - -// Export the TypeScript type -export type V12ChartsConfigSchema = z.infer; - -// Export individual schemas for potential reuse -export { - v12ColorSchema, - v12ShapeSchema, - v12TooltipSchema, - v12LabelSchema, - v12FilterSchema, - v12SortSchema, - v12LinkSchema, - v12VisualizationSchema, - v12UpdateSchema, - v12FieldSchema, - v12HierarchyFieldSchema, - v12PointSizeConfigSchema, - v12ColorsConfigSchema, - v12ShapesConfigSchema, - v12TooltipConfigSchema, - v12CommonSharedExtraSettingsSchema, - v12NavigatorSettingsSchema, - v12FormattingSchema, - v12ClientOnlyFieldsSchema, - v12ChartsConfigDatasetFieldSchema, -}; From 5c2d0ce8969604a57c168bc8a4a526be359b02a6 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Mon, 15 Sep 2025 14:44:46 +0300 Subject: [PATCH 34/40] Fix ts --- src/shared/schema/mix/types/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shared/schema/mix/types/index.ts b/src/shared/schema/mix/types/index.ts index 7de86b74d3..90757ac7a7 100644 --- a/src/shared/schema/mix/types/index.ts +++ b/src/shared/schema/mix/types/index.ts @@ -2,4 +2,3 @@ export * from './navigation'; export * from './entries'; export * from './markdown'; export * from './dash'; -export * from './editor'; From 74cc9c014c2d6dd6a167d1cf82da4241903b048b Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Mon, 15 Sep 2025 15:04:30 +0300 Subject: [PATCH 35/40] Fixes --- .../charts-engine/components/storage/united-storage/provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/components/charts-engine/components/storage/united-storage/provider.ts b/src/server/components/charts-engine/components/storage/united-storage/provider.ts index 49854261a6..1b4f5f2f26 100644 --- a/src/server/components/charts-engine/components/storage/united-storage/provider.ts +++ b/src/server/components/charts-engine/components/storage/united-storage/provider.ts @@ -610,7 +610,7 @@ export class USProvider { recursion: boolean; links?: unknown; meta: Record; - workbookId: string | null; + workbookId: string; name: string; includePermissionsInfo?: boolean; mode: EntryUpdateMode; From 650278d6fd4e94adedca8f6de430de7b5966892d Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Mon, 15 Sep 2025 15:14:08 +0300 Subject: [PATCH 36/40] Revert "Fixes" This reverts commit 74cc9c014c2d6dd6a167d1cf82da4241903b048b. --- .../charts-engine/components/storage/united-storage/provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/components/charts-engine/components/storage/united-storage/provider.ts b/src/server/components/charts-engine/components/storage/united-storage/provider.ts index 1b4f5f2f26..49854261a6 100644 --- a/src/server/components/charts-engine/components/storage/united-storage/provider.ts +++ b/src/server/components/charts-engine/components/storage/united-storage/provider.ts @@ -610,7 +610,7 @@ export class USProvider { recursion: boolean; links?: unknown; meta: Record; - workbookId: string; + workbookId: string | null; name: string; includePermissionsInfo?: boolean; mode: EntryUpdateMode; From 6676af3b839730931ba8096e5e163b5c5bc53399 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Mon, 15 Sep 2025 15:14:09 +0300 Subject: [PATCH 37/40] Revert "Fix ts" This reverts commit 5c2d0ce8969604a57c168bc8a4a526be359b02a6. --- src/shared/schema/mix/types/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/schema/mix/types/index.ts b/src/shared/schema/mix/types/index.ts index 90757ac7a7..7de86b74d3 100644 --- a/src/shared/schema/mix/types/index.ts +++ b/src/shared/schema/mix/types/index.ts @@ -2,3 +2,4 @@ export * from './navigation'; export * from './entries'; export * from './markdown'; export * from './dash'; +export * from './editor'; From 9964995e81cf941494c2f7e908e89ba56ab987d5 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Mon, 15 Sep 2025 15:14:10 +0300 Subject: [PATCH 38/40] Revert "Remove not ready schemas" This reverts commit 4dee5d0588b93ab4351551202b3214c8b123abe4. --- .../storage/united-storage/provider.ts | 26 +- src/server/components/sdk/dash.ts | 9 +- .../__tests__/dash-api.schema.test.ts | 989 ++++++++++++++++++ src/shared/zod-schemas/dash.ts | 285 +++++ src/shared/zod-schemas/wizard.ts | 496 +++++++++ 5 files changed, 1802 insertions(+), 3 deletions(-) create mode 100644 src/shared/zod-schemas/__tests__/dash-api.schema.test.ts create mode 100644 src/shared/zod-schemas/dash.ts create mode 100644 src/shared/zod-schemas/wizard.ts diff --git a/src/server/components/charts-engine/components/storage/united-storage/provider.ts b/src/server/components/charts-engine/components/storage/united-storage/provider.ts index 49854261a6..af31dd0c5e 100644 --- a/src/server/components/charts-engine/components/storage/united-storage/provider.ts +++ b/src/server/components/charts-engine/components/storage/united-storage/provider.ts @@ -27,6 +27,7 @@ import { TRACE_ID_HEADER, US_PUBLIC_API_TOKEN_HEADER, WORKBOOK_ID_HEADER, + mapChartsConfigToLatestVersion, } from '../../../../../../shared'; import {ErrorCode, TIMEOUT_10_SEC} from '../../../../../../shared/constants'; import {createErrorHandler} from '../../error-handler'; @@ -226,7 +227,7 @@ export type ProviderCreateParams = { recursion?: boolean; meta?: Record; includePermissionsInfo?: boolean | string; - workbookId: string; + workbookId: string | null; name: string; mode?: EntryUpdateMode; annotation?: EntryAnnotationArgs; @@ -357,6 +358,29 @@ export class USProvider { }); } + static async retrieveParsedWizardChart( + ctx: AppContext, + props: { + id: string; + storageApiPath?: string; + extraAllowedHeaders?: string[]; + unreleased: boolean | string; + includeLinks?: boolean | string; + includePermissionsInfo?: boolean | string; + revId?: string; + headers: Request['headers']; + workbookId?: WorkbookId; + includeServicePlan?: boolean; + includeTenantFeatures?: boolean; + }, + ) { + const result = await USProvider.retrieveById(ctx, props); + + result.data = mapChartsConfigToLatestVersion(JSON.parse(result.data.shared)) as any; + + return result; + } + static retrieveByKey( ctx: AppContext, { diff --git a/src/server/components/sdk/dash.ts b/src/server/components/sdk/dash.ts index 30e737c53b..3b56d02552 100644 --- a/src/server/components/sdk/dash.ts +++ b/src/server/components/sdk/dash.ts @@ -283,6 +283,7 @@ class Dash { params: EntryReadParams | null, headers: IncomingHttpHeaders, ctx: AppContext, + options?: {forceMigrate?: boolean}, ): Promise { try { const headersWithMetadata = { @@ -297,7 +298,10 @@ class Dash { const isServerMigrationEnabled = Boolean( isEnabledServerFeature(Feature.DashServerMigrationEnable), ); - if (isServerMigrationEnabled && DashSchemeConverter.isUpdateNeeded(result.data)) { + if ( + (options?.forceMigrate || isServerMigrationEnabled) && + DashSchemeConverter.isUpdateNeeded(result.data) + ) { result.data = await Dash.migrate(result.data); } @@ -398,6 +402,7 @@ class Dash { headers: IncomingHttpHeaders, ctx: AppContext, I18n: ServerI18n, + options?: {forceMigrate?: boolean}, ): Promise { try { const usData: typeof data & {skipSyncLinks?: boolean} = {...data}; @@ -406,7 +411,7 @@ class Dash { const needDataSend = !(mode === EntryUpdateMode.Publish && data.revId); if (needDataSend) { if (needSetDefaultData(usData.data)) { - const initialData = await Dash.read(entryId, null, headers, ctx); + const initialData = await Dash.read(entryId, null, headers, ctx, options); usData.data = setDefaultData(I18n, usData.data, initialData.data); } diff --git a/src/shared/zod-schemas/__tests__/dash-api.schema.test.ts b/src/shared/zod-schemas/__tests__/dash-api.schema.test.ts new file mode 100644 index 0000000000..0db557d101 --- /dev/null +++ b/src/shared/zod-schemas/__tests__/dash-api.schema.test.ts @@ -0,0 +1,989 @@ +import { + CONTROLS_PLACEMENT_MODE, + DASH_CURRENT_SCHEME_VERSION, + DashLoadPriority, + DashTabConnectionKind, + DashTabItemControlElementType, + DashTabItemControlSourceType, + DashTabItemTitleSizes, + DashTabItemType, +} from '../..'; +import {type DashSchema, dashSchema} from '../dash'; + +const DASH_DEFAULT_NAMESPACE = 'default'; + +describe('dashSchema', () => { + describe('valid configurations', () => { + it('should validate minimal valid dashboard configuration', () => { + const validConfig: DashSchema = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(validConfig); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(validConfig); + } + }); + + it('should validate complete dashboard configuration with all optional fields', () => { + const validConfig: DashSchema = { + key: 'dashboard-key-123', + data: { + counter: 1, + salt: 'random-salt-123', + schemeVersion: DASH_CURRENT_SCHEME_VERSION, + tabs: [ + { + id: 'tab1', + title: 'Complete Tab', + items: [ + { + id: 'text1', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Text, + data: { + text: 'Sample text content', + }, + }, + { + id: 'title1', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Title, + data: { + text: 'Sample Title', + size: DashTabItemTitleSizes.L, + showInTOC: true, + }, + }, + { + id: 'widget1', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Widget, + data: { + hideTitle: false, + tabs: [ + { + id: 'widget-tab1', + title: 'Widget Tab', + description: 'Widget description', + chartId: 'chart123', + isDefault: true, + params: {param1: 'value1'}, + autoHeight: true, + }, + ], + }, + }, + ], + layout: [ + { + i: 'text1', + h: 2, + w: 12, + x: 0, + y: 0, + }, + { + i: 'title1', + h: 1, + w: 12, + x: 0, + y: 2, + parent: 'text1', + }, + ], + connections: [ + { + from: 'control1', + to: 'widget1', + kind: DashTabConnectionKind.Ignore, + }, + ], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [['alias1', 'alias2']], + }, + }, + ], + settings: { + autoupdateInterval: 60, + maxConcurrentRequests: 5, + loadPriority: DashLoadPriority.Charts, + silentLoading: true, + dependentSelectors: false, + globalParams: {globalParam: 'value'}, + hideTabs: false, + hideDashTitle: true, + expandTOC: false, + }, + }, + meta: {metaKey: 'metaValue'}, + links: {linkKey: 'linkValue'}, + }; + + const result = dashSchema.safeParse(validConfig); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(validConfig); + } + }); + + it('should validate dashboard with control items', () => { + const validConfig: DashSchema = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Tab with Controls', + items: [ + { + id: 'control1', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Control, + data: { + title: 'Dataset Control', + sourceType: DashTabItemControlSourceType.Dataset, + source: { + datasetId: 'dataset123', + datasetFieldId: 'field123', + elementType: DashTabItemControlElementType.Select, + required: true, + showHint: true, + showTitle: true, + defaultValue: 'default', + multiselectable: false, + }, + }, + defaults: {defaultParam: 'value'}, + }, + { + id: 'control2', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Control, + data: { + title: 'Manual Control', + sourceType: DashTabItemControlSourceType.Manual, + source: { + fieldName: 'manualField', + elementType: DashTabItemControlElementType.Date, + required: false, + showHint: false, + showTitle: true, + defaultValue: '2023-01-01', + isRange: true, + acceptableValues: { + from: '2023-01-01', + to: '2023-12-31', + }, + }, + }, + defaults: {}, + }, + { + id: 'control3', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Control, + data: { + title: 'External Control', + sourceType: DashTabItemControlSourceType.External, + source: { + chartId: 'external-chart-123', + text: 'External control text', + autoHeight: true, + }, + }, + defaults: {}, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(validConfig); + expect(result.success).toBe(true); + }); + + it('should validate dashboard with group control', () => { + const validConfig: DashSchema = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Tab with Group Control', + items: [ + { + id: 'groupControl1', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.GroupControl, + data: { + group: [ + { + id: 'groupItem1', + title: 'Group Item 1', + namespace: DASH_DEFAULT_NAMESPACE, + sourceType: DashTabItemControlSourceType.Dataset, + defaults: {param: 'value'}, + placementMode: CONTROLS_PLACEMENT_MODE.AUTO, + width: '50%', + source: { + datasetId: 'dataset123', + datasetFieldId: 'field123', + elementType: + DashTabItemControlElementType.Input, + required: true, + showHint: false, + showTitle: true, + defaultValue: 'input value', + }, + }, + ], + autoHeight: true, + buttonApply: true, + buttonReset: false, + showGroupName: true, + updateControlsOnChange: false, + }, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(validConfig); + expect(result.success).toBe(true); + }); + + it('should validate different control element types', () => { + // Test Select control + const selectConfig: DashSchema = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [ + { + id: 'control-select', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Control, + data: { + title: 'Select Control', + sourceType: DashTabItemControlSourceType.Manual, + source: { + fieldName: 'selectField', + elementType: DashTabItemControlElementType.Select, + required: false, + showHint: true, + showTitle: true, + defaultValue: ['option1', 'option2'], + multiselectable: true, + }, + }, + defaults: {}, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + // Test Date control + const dateConfig: DashSchema = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [ + { + id: 'control-date', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Control, + data: { + title: 'Date Control', + sourceType: DashTabItemControlSourceType.Manual, + source: { + fieldName: 'dateField', + elementType: DashTabItemControlElementType.Date, + required: false, + showHint: true, + showTitle: true, + defaultValue: '2023-01-01', + isRange: false, + }, + }, + defaults: {}, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + // Test Input control + const inputConfig: DashSchema = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [ + { + id: 'control-input', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Control, + data: { + title: 'Input Control', + sourceType: DashTabItemControlSourceType.Manual, + source: { + fieldName: 'inputField', + elementType: DashTabItemControlElementType.Input, + required: false, + showHint: true, + showTitle: true, + defaultValue: 'input text', + }, + }, + defaults: {}, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + // Test Checkbox control + const checkboxConfig: DashSchema = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [ + { + id: 'control-checkbox', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Control, + data: { + title: 'Checkbox Control', + sourceType: DashTabItemControlSourceType.Manual, + source: { + fieldName: 'checkboxField', + elementType: DashTabItemControlElementType.Checkbox, + required: false, + showHint: true, + showTitle: true, + defaultValue: 'true', + }, + }, + defaults: {}, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const configs = [selectConfig, dateConfig, inputConfig, checkboxConfig]; + configs.forEach((config) => { + const result = dashSchema.safeParse(config); + expect(result.success).toBe(true); + }); + }); + + it('should validate different title sizes', () => { + const titleSizes = Object.values(DashTabItemTitleSizes); + + titleSizes.forEach((size, index) => { + const config: DashSchema = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [ + { + id: `title${index}`, + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Title, + data: { + text: `Title ${size}`, + size: size, + }, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(config); + expect(result.success).toBe(true); + }); + }); + }); + + describe('invalid configurations', () => { + it('should reject configuration without required data field', () => { + const invalidConfig = { + key: 'test-key', + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'invalid_type', + expected: 'object', + message: 'Invalid input: expected object, received undefined', + path: ['data'], + }), + ); + } + }); + + it('should reject configuration without tabs', () => { + const invalidConfig = { + data: { + settings: {}, + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'invalid_type', + path: ['data', 'tabs'], + message: 'Invalid input: expected array, received undefined', + }), + ); + } + }); + + it('should reject tab with empty id', () => { + const invalidConfig = { + data: { + tabs: [ + { + id: '', + title: 'Test Tab', + items: [], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'too_small', + path: ['data', 'tabs', 0, 'id'], + }), + ); + } + }); + + it('should reject tab with empty title', () => { + const invalidConfig = { + data: { + tabs: [ + { + id: 'tab1', + title: '', + items: [], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'too_small', + path: ['data', 'tabs', 0, 'title'], + }), + ); + } + }); + + it('should reject item with invalid namespace', () => { + const invalidConfig = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [ + { + id: 'text1', + namespace: 'invalid-namespace', + type: DashTabItemType.Text, + data: { + text: 'Sample text', + }, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'invalid_value', + path: ['data', 'tabs', 0, 'items', 0, 'namespace'], + }), + ); + } + }); + + it('should reject control with invalid element type', () => { + const invalidConfig = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [ + { + id: 'control1', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Control, + data: { + title: 'Invalid Control', + sourceType: DashTabItemControlSourceType.Manual, + source: { + fieldName: 'field1', + elementType: 'invalid-type', + required: false, + showHint: false, + showTitle: true, + }, + }, + defaults: {}, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + }); + + it('should reject widget tab with empty chartId', () => { + const invalidConfig = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [ + { + id: 'widget1', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Widget, + data: { + hideTitle: false, + tabs: [ + { + id: 'widget-tab1', + title: 'Widget Tab', + chartId: '', + }, + ], + }, + }, + ], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'too_small', + path: ['data', 'tabs', 0, 'items', 0, 'data', 'tabs', 0, 'chartId'], + }), + ); + } + }); + + it('should reject invalid scheme version', () => { + const invalidConfig = { + data: { + schemeVersion: DASH_CURRENT_SCHEME_VERSION + 1, + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'too_big', + path: ['data', 'schemeVersion'], + }), + ); + } + }); + + it('should reject invalid autoupdate interval', () => { + const invalidConfig = { + data: { + tabs: [], + settings: { + autoupdateInterval: 15, // less than minimum 30 + }, + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'invalid_union', + path: ['data', 'settings', 'autoupdateInterval'], + }), + ); + } + }); + + it('should reject invalid maxConcurrentRequests', () => { + const invalidConfig = { + data: { + tabs: [], + settings: { + maxConcurrentRequests: 0, // less than minimum 1 + }, + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'invalid_union', + path: ['data', 'settings', 'maxConcurrentRequests'], + }), + ); + } + }); + + it('should accept layout item with negative dimensions (schema allows this)', () => { + const validConfig = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [], + layout: [ + { + i: 'item1', + h: -1, + w: 12, + x: 0, + y: 0, + }, + ], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(validConfig); + expect(result.success).toBe(true); + }); + + it('should reject connection with empty from/to fields', () => { + const invalidConfig = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [], + layout: [], + connections: [ + { + from: '', + to: 'widget1', + kind: DashTabConnectionKind.Ignore, + }, + ], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'too_small', + path: ['data', 'tabs', 0, 'connections', 0, 'from'], + }), + ); + } + }); + + it('should reject aliases with invalid structure', () => { + const invalidConfig = { + data: { + tabs: [ + { + id: 'tab1', + title: 'Test Tab', + items: [], + layout: [], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [['single-item']], // should have at least 2 items + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'too_small', + path: ['data', 'tabs', 0, 'aliases', DASH_DEFAULT_NAMESPACE, 0], + }), + ); + } + }); + }); + + describe('edge cases', () => { + it('should handle null values in settings', () => { + const config = { + data: { + tabs: [], + settings: { + autoupdateInterval: null, + maxConcurrentRequests: null, + }, + }, + }; + + const result = dashSchema.safeParse(config); + expect(result.success).toBe(true); + }); + + it('should handle string autoupdate interval', () => { + const config = { + data: { + tabs: [], + settings: { + autoupdateInterval: '60', + }, + }, + }; + + const result = dashSchema.safeParse(config); + expect(result.success).toBe(true); + }); + + it('should handle empty key', () => { + const config = { + key: '', + data: { + tabs: [], + }, + }; + + const result = dashSchema.safeParse(config); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + code: 'too_small', + path: ['key'], + }), + ); + } + }); + + it('should handle complex nested structures', () => { + const config: DashSchema = { + data: { + tabs: [ + { + id: 'complex-tab', + title: 'Complex Tab', + items: [ + { + id: 'complex-widget', + namespace: DASH_DEFAULT_NAMESPACE, + type: DashTabItemType.Widget, + data: { + hideTitle: false, + tabs: [ + { + id: 'nested-tab-1', + title: 'Nested Tab 1', + description: 'First nested tab', + chartId: 'chart-1', + isDefault: true, + params: { + complexParam: { + nested: { + value: 'deep-value', + array: [1, 2, 3], + }, + }, + }, + autoHeight: false, + }, + { + id: 'nested-tab-2', + title: 'Nested Tab 2', + chartId: 'chart-2', + params: {}, + }, + ], + }, + }, + ], + layout: [ + { + i: 'complex-widget', + h: 10, + w: 24, + x: 6, + y: 5, + }, + ], + connections: [], + aliases: { + [DASH_DEFAULT_NAMESPACE]: [ + ['alias-group-1', 'alias-group-2'], + ['another-alias-1', 'another-alias-2'], + ], + }, + }, + ], + }, + }; + + const result = dashSchema.safeParse(config); + expect(result.success).toBe(true); + }); + }); +}); diff --git a/src/shared/zod-schemas/dash.ts b/src/shared/zod-schemas/dash.ts new file mode 100644 index 0000000000..d8dbcf77cc --- /dev/null +++ b/src/shared/zod-schemas/dash.ts @@ -0,0 +1,285 @@ +import * as z from 'zod/v4'; + +import { + CONTROLS_PLACEMENT_MODE, + DASH_CURRENT_SCHEME_VERSION, + DashLoadPriority, + DashTabConnectionKind, + DashTabItemControlElementType, + DashTabItemControlSourceType, + DashTabItemTitleSizes, + DashTabItemType, +} from '..'; + +const DASH_DEFAULT_NAMESPACE = 'default'; + +// Text definition +const textSchema = z.object({ + text: z.string(), +}); + +// Title definition +const titleSchema = z.object({ + text: z.string(), + size: z.enum(DashTabItemTitleSizes), + showInTOC: z.boolean().optional(), +}); + +// Widget definition +const widgetSchema = z.object({ + hideTitle: z.boolean().optional(), + tabs: z.array( + z.object({ + id: z.string().min(1), + title: z.string().min(1), + description: z.string().optional(), + chartId: z.string().min(1).optional(), + isDefault: z.boolean().optional(), + params: z.record(z.any(), z.any()).optional(), + autoHeight: z.boolean().optional(), + }), + ), +}); + +// Control element type definition +const controlElementTypeSchema = z + .object({ + required: z.boolean().optional(), + showHint: z.boolean().optional(), + showTitle: z.boolean().optional(), + elementType: z.enum(DashTabItemControlElementType), + }) + .and( + z.discriminatedUnion('elementType', [ + z.object({ + elementType: z.literal(DashTabItemControlElementType.Select), + defaultValue: z.union([z.string(), z.array(z.string()), z.null()]).optional(), + multiselectable: z.boolean().optional(), + }), + z.object({ + elementType: z.literal(DashTabItemControlElementType.Date), + defaultValue: z.string().optional(), + isRange: z.boolean().optional(), + }), + z.object({ + elementType: z.literal(DashTabItemControlElementType.Input), + defaultValue: z.string().optional(), + }), + z.object({ + elementType: z.literal(DashTabItemControlElementType.Checkbox), + defaultValue: z.string().optional(), + }), + ]), + ); + +// Control source dataset definition +const controlSourceDatasetSchema = controlElementTypeSchema.and( + z.object({ + datasetId: z.string().min(1), + datasetFieldId: z.string().min(1), + }), +); + +// Control source manual definition +const controlSourceManualSchema = controlElementTypeSchema.and( + z.object({ + fieldName: z.string().min(1), + acceptableValues: z + .union([ + // elementType: select + z.array( + z.object({ + value: z.string(), + title: z.string(), + }), + ), + // elementType: date + z.object({ + from: z.string().optional(), + to: z.string().optional(), + }), + ]) + .optional(), + }), +); + +// Control source external definition +const controlSourceExternalSchema = z.object({ + chartId: z.string().min(1), + text: z.string().optional(), + autoHeight: z.boolean().optional(), +}); + +// Control definition +const controlSchema = z + .object({ + title: z.string().min(1), + sourceType: z.enum(DashTabItemControlSourceType), + }) + .and( + z.discriminatedUnion('sourceType', [ + z.object({ + sourceType: z.literal(DashTabItemControlSourceType.Dataset), + source: controlSourceDatasetSchema, + }), + z.object({ + sourceType: z.literal(DashTabItemControlSourceType.Manual), + source: controlSourceManualSchema, + }), + z.object({ + sourceType: z.literal(DashTabItemControlSourceType.External), + source: controlSourceExternalSchema, + }), + ]), + ); + +// Group control items definition +const groupControlItemsSchema = z + .object({ + id: z.string().min(1), + title: z.string().min(1), + namespace: z.literal(DASH_DEFAULT_NAMESPACE), + sourceType: z.union([ + z.literal(DashTabItemControlSourceType.Dataset), + z.literal(DashTabItemControlSourceType.Manual), + ]), + defaults: z.record(z.any(), z.any()), + placementMode: z.enum(CONTROLS_PLACEMENT_MODE).optional(), + width: z.string().optional(), + }) + .and( + z.discriminatedUnion('sourceType', [ + z.object({ + sourceType: z.literal(DashTabItemControlSourceType.Dataset), + source: controlSourceDatasetSchema, + }), + z.object({ + sourceType: z.literal(DashTabItemControlSourceType.Manual), + source: controlSourceManualSchema, + }), + ]), + ); + +// Group control definition +const groupControlSchema = z.object({ + group: z.array(groupControlItemsSchema), + autoHeight: z.boolean().optional(), + buttonApply: z.boolean().optional(), + buttonReset: z.boolean().optional(), + showGroupName: z.boolean().optional(), + updateControlsOnChange: z.boolean().optional(), +}); + +// Layout item definition +const layoutItemSchema = z.object({ + i: z.string().min(1), + h: z.number(), + w: z.number(), + x: z.number(), + y: z.number(), + parent: z.string().optional(), +}); + +// Connection definition +const connectionSchema = z.object({ + from: z.string().min(1), + to: z.string().min(1), + kind: z.enum(DashTabConnectionKind), +}); + +// Tab item definition +const tabItemSchema = z + .object({ + id: z.string().min(1), + namespace: z.literal(DASH_DEFAULT_NAMESPACE), + type: z.enum(DashTabItemType), + }) + .and( + z.discriminatedUnion('type', [ + z.object({ + type: z.literal(DashTabItemType.Text), + data: textSchema, + }), + z.object({ + type: z.literal(DashTabItemType.Title), + data: titleSchema, + }), + z.object({ + type: z.literal(DashTabItemType.Widget), + data: widgetSchema, + }), + z.object({ + type: z.literal(DashTabItemType.Control), + data: controlSchema, + defaults: z.record(z.any(), z.any()), + }), + z.object({ + type: z.literal(DashTabItemType.GroupControl), + data: groupControlSchema, + }), + ]), + ); + +// Alias definition +const aliasRecordSchema = z.array(z.string().min(1)).max(2).min(2); + +// Tab definition +const tabSchema = z + .object({ + id: z.string().min(1), + title: z.string().min(1), + items: z.array(tabItemSchema), + layout: z.array(layoutItemSchema), + connections: z.array(connectionSchema), + aliases: z + .object({ + [DASH_DEFAULT_NAMESPACE]: z.array(aliasRecordSchema).optional(), + }) + .strict(), + }) + .strict(); + +// Settings definition +const settingsSchema = z.object({ + autoupdateInterval: z.union([z.number().min(30), z.string(), z.null()]).optional(), + maxConcurrentRequests: z.union([z.number().min(1), z.null()]).optional(), + loadPriority: z.enum(DashLoadPriority).optional(), + silentLoading: z.boolean().optional(), + dependentSelectors: z.boolean().optional(), + globalParams: z.record(z.any(), z.any()).optional(), + hideTabs: z.boolean().optional(), + hideDashTitle: z.boolean().optional(), + expandTOC: z.boolean().optional(), + assistantEnabled: z.boolean().optional(), +}); + +// Data definition +const dataSchema = z.object({ + counter: z.number().int().min(1).optional(), + salt: z.string().min(1).optional(), + schemeVersion: z + .number() + .int() + .min(1) + .max(DASH_CURRENT_SCHEME_VERSION) + .default(DASH_CURRENT_SCHEME_VERSION) + .optional(), + tabs: z.array(tabSchema), + settings: settingsSchema.optional(), + supportDescription: z.string().optional(), + accessDescription: z.string().optional(), +}); + +// Main dashboard API validation schema +export const dashSchema = z.object({ + key: z.string().min(1).optional(), + workbookId: z.union([z.null(), z.string()]).optional(), + data: dataSchema, + meta: z.record(z.any(), z.any()).optional(), + links: z.record(z.string(), z.string()).optional(), +}); + +export const dashApiValidationJsonSchema = z.toJSONSchema(dashSchema); + +// Export the type for TypeScript usage +export type DashSchema = z.infer; diff --git a/src/shared/zod-schemas/wizard.ts b/src/shared/zod-schemas/wizard.ts new file mode 100644 index 0000000000..e20f317c68 --- /dev/null +++ b/src/shared/zod-schemas/wizard.ts @@ -0,0 +1,496 @@ +import * as z from 'zod/v4'; + +import { + ColorMode, + GradientNullModes, + IndicatorTitleMode, + LabelsPositions, + MapCenterMode, + ZoomMode, +} from '..'; +import {WidgetSize} from '../constants'; +import {MARKUP_TYPE} from '../types/charts'; +import type {DatasetFieldCalcMode} from '../types/dataset'; +import { + AxisLabelFormatMode, + AxisMode, + AxisNullsMode, + ChartsConfigVersion, + NumberFormatType, + NumberFormatUnit, +} from '../types/wizard'; + +// Helper type for enum to literal conversion +type EnumToLiteral = T extends string + ? `${T}` + : `${T}` extends `${infer N extends number}` + ? N + : never; + +type ValueOf = T[keyof T]; + +// Constants for enum arrays - base arrays +const showHideValuesList = ['show', 'hide'] as const; +const onOffValuesList = ['on', 'off'] as const; +const autoManualValuesList = ['auto', 'manual'] as const; +const yesNoValuesList = ['yes', 'no'] as const; + +// Constants for literal values +const SCALE_VALUE_LITERAL = '0-max'; +const LOGARITHMIC_TYPE_LITERAL = 'logarithmic'; +const MANUAL_GRID_STEP_LITERAL = 'manual'; +const DATALENS_TYPE_LITERAL = 'datalens'; + +// Constants for enum arrays - specific arrays +const DatasetFieldCalcModeList: DatasetFieldCalcMode[] = ['formula', 'direct', 'parameter']; +const titleModeList = showHideValuesList; +const legendModeList = showHideValuesList; +const tooltipModeList = showHideValuesList; +const onOffList = onOffValuesList; +const groupingList = ['disabled', 'off'] as const; +const scaleList = autoManualValuesList; +const titleList = ['auto', 'manual', 'off'] as const; +const gridList = onOffValuesList; +const hideLabelsLis = yesNoValuesList; +const labelsViewList = ['horizontal', 'vertical', 'angle'] as const; +const holidaysList = onOffValuesList; +const axisVisibilityList = showHideValuesList; +const colorFieldTitleList = onOffValuesList; +const v12UpdateActionsList = [ + 'add_field', + 'add', + 'update_field', + 'update', + 'delete', + 'delete_field', +] as const; + +// Basic schemas +const parameterDefaultValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +const datasetFieldCalcModeSchema = z.enum(DatasetFieldCalcModeList); + +// Enum schemas using Object.values for regular enums +const colorModeSchema = z.enum(Object.values(ColorMode) as EnumToLiteral[]); +const gradientNullModeSchema = z.enum( + Object.values(GradientNullModes) as ValueOf[], +); +const indicatorTitleModeSchema = z.enum([ + IndicatorTitleMode.ByField, + IndicatorTitleMode.Hide, + IndicatorTitleMode.Manual, +]); +const labelsPositionsSchema = z.enum( + Object.values(LabelsPositions) as EnumToLiteral[], +); +const markupTypeSchema = z.enum( + Object.values(MARKUP_TYPE) as [ + EnumToLiteral<(typeof MARKUP_TYPE)[keyof typeof MARKUP_TYPE]>, + ...EnumToLiteral<(typeof MARKUP_TYPE)[keyof typeof MARKUP_TYPE]>[], + ], +); +const widgetSizeTypeSchema = z.enum( + Object.values(WidgetSize) as [ + EnumToLiteral<(typeof WidgetSize)[keyof typeof WidgetSize]>, + ...EnumToLiteral<(typeof WidgetSize)[keyof typeof WidgetSize]>[], + ], +); + +// Const enum schemas using direct values +const axisLabelFormatModeSchema = z.enum([AxisLabelFormatMode.Auto, AxisLabelFormatMode.ByField]); +const axisModeSchema = z.enum([AxisMode.Discrete, AxisMode.Continuous]); +const axisNullsModeSchema = z.enum([ + AxisNullsMode.Ignore, + AxisNullsMode.Connect, + AxisNullsMode.AsZero, + AxisNullsMode.UsePrevious, +]); +const numberFormatTypeSchema = z.enum([NumberFormatType.Number, NumberFormatType.Percent]); +const numberFormatUnitSchema = z.enum([ + NumberFormatUnit.Auto, + NumberFormatUnit.B, + NumberFormatUnit.K, + NumberFormatUnit.M, + NumberFormatUnit.T, +]); + +// Simple string enums for types that are not available as runtime values +const mapCenterModesSchema = z.enum([MapCenterMode.Auto, MapCenterMode.Manual]); +const zoomModesSchema = z.enum([ZoomMode.Auto, ZoomMode.Manual]); + +// V12ClientOnlyFields schema +const v12ClientOnlyFieldsSchema = z.object({ + fakeTitle: z.string().optional(), + originalTitle: z.string().optional(), + markupType: markupTypeSchema.optional(), +}); + +// V12Formatting schema +const v12FormattingSchema = z.object({ + format: numberFormatTypeSchema.optional(), + showRankDelimiter: z.boolean().optional(), + prefix: z.string().optional(), + postfix: z.string().optional(), + unit: numberFormatUnitSchema.optional(), + precision: z.number().optional(), + labelMode: z.string().optional(), +}); + +// V12NavigatorSettings schema +const v12NavigatorSettingsSchema = z.object({ + navigatorMode: z.string(), + isNavigatorAvailable: z.boolean(), + selectedLines: z.array(z.string()), + linesMode: z.string(), + periodSettings: z.object({ + type: z.string(), + value: z.string(), + period: z.string(), + }), +}); + +// V12CommonSharedExtraSettings schema +const v12CommonSharedExtraSettingsSchema = z.object({ + title: z.string().optional(), + titleMode: z.enum(titleModeList).optional(), + indicatorTitleMode: indicatorTitleModeSchema.optional(), + legendMode: z.enum(legendModeList).optional(), + metricFontSize: z.string().optional(), + metricFontColor: z.string().optional(), + tooltip: z.enum(tooltipModeList).optional(), + tooltipSum: z.enum(onOffList).optional(), + limit: z.number().optional(), + pagination: z.enum(onOffList).optional(), + navigatorMode: z.string().optional(), + navigatorSeriesName: z.string().optional(), + totals: z.enum(onOffList).optional(), + pivotFallback: z.enum(onOffList).optional(), + pivotInlineSort: z.enum(onOffList).optional(), + stacking: z.enum(onOffList).optional(), + overlap: z.enum(onOffList).optional(), + feed: z.string().optional(), + navigatorSettings: v12NavigatorSettingsSchema.optional(), + enableGPTInsights: z.boolean().optional(), + labelsPosition: labelsPositionsSchema.optional(), + pinnedColumns: z.number().optional(), + size: widgetSizeTypeSchema.optional(), + zoomMode: zoomModesSchema.optional(), + zoomValue: z.number().nullable().optional(), + mapCenterMode: mapCenterModesSchema.optional(), + mapCenterValue: z.string().nullable().optional(), + preserveWhiteSpace: z.boolean().optional(), +}); + +// V12Filter schema +const v12FilterSchema = z + .object({ + guid: z.string(), + datasetId: z.string(), + disabled: z.string().optional(), + filter: z.object({ + operation: z.object({ + code: z.string(), + }), + value: z.union([z.string(), z.array(z.string())]).optional(), + }), + type: z.string(), + title: z.string(), + calc_mode: datasetFieldCalcModeSchema, + }) + .merge(v12ClientOnlyFieldsSchema); + +// V12Sort schema +const v12SortSchema = z + .object({ + guid: z.string(), + title: z.string(), + source: z.string().optional(), + datasetId: z.string(), + direction: z.string(), + data_type: z.string(), + format: z.string().optional(), + type: z.string(), + default_value: parameterDefaultValueSchema.optional(), + }) + .merge(v12ClientOnlyFieldsSchema); + +// V12LinkField schema +const v12LinkFieldSchema = z.object({ + field: z.object({ + title: z.string(), + guid: z.string(), + }), + dataset: z.object({ + id: z.string(), + realName: z.string(), + }), +}); + +// V12Link schema +const v12LinkSchema = z.object({ + id: z.string(), + fields: z.record(z.string(), v12LinkFieldSchema), +}); + +// V12PlaceholderSettings schema +const v12PlaceholderSettingsSchema = z.object({ + groupping: z.enum(groupingList).optional(), + autoscale: z.boolean().optional(), + scale: z.enum(scaleList).optional(), + scaleValue: z + .union([z.literal(SCALE_VALUE_LITERAL), z.tuple([z.string(), z.string()])]) + .optional(), + title: z.enum(titleList).optional(), + titleValue: z.string().optional(), + type: z.literal(LOGARITHMIC_TYPE_LITERAL).optional(), + grid: z.enum(gridList).optional(), + gridStep: z.literal(MANUAL_GRID_STEP_LITERAL).optional(), + gridStepValue: z.number().optional(), + hideLabels: z.enum(hideLabelsLis).optional(), + labelsView: z.enum(labelsViewList).optional(), + nulls: axisNullsModeSchema.optional(), + holidays: z.enum(holidaysList).optional(), + axisFormatMode: axisLabelFormatModeSchema.optional(), + axisModeMap: z.record(z.string(), axisModeSchema).optional(), + disableAxisMode: z.boolean().optional(), + axisVisibility: z.enum(axisVisibilityList).optional(), +}); + +// Forward declaration for recursive types +const v12FieldSchemaInner = z.object({ + ...v12ClientOnlyFieldsSchema.shape, + data_type: z.string(), + type: z.string(), + title: z.string(), + guid: z.string(), + formatting: v12FormattingSchema.optional(), + format: z.string().optional(), + datasetId: z.string(), + source: z.string().optional(), + datasetName: z.string().optional(), + hideLabelMode: z.string().optional(), + calc_mode: datasetFieldCalcModeSchema, + default_value: parameterDefaultValueSchema.optional(), + barsSettings: z.any().optional(), // TableBarsSettings + subTotalsSettings: z.any().optional(), // TableSubTotalsSettings + backgroundSettings: z.any().optional(), // TableFieldBackgroundSettings + columnSettings: z.any().optional(), // ColumnSettings + hintSettings: z.any().optional(), // HintSettings +}); + +const v12FieldSchema = z.object({ + ...v12FieldSchemaInner.shape, + fields: z.array(v12FieldSchemaInner).optional(), +}); + +// V12Placeholder schema +const v12PlaceholderSchema = z.object({ + id: z.string(), + settings: v12PlaceholderSettingsSchema.optional(), + required: z.boolean().optional(), + capacity: z.number().optional(), + items: z.array(v12FieldSchema), +}); + +// V12Color schema +const v12ColorSchema = z + .object({ + datasetId: z.string(), + guid: z.string(), + title: z.string(), + type: z.string(), + data_type: z.string(), + formatting: v12FormattingSchema.optional(), + calc_mode: datasetFieldCalcModeSchema, + }) + .merge(v12ClientOnlyFieldsSchema); + +// V12Shape schema +const v12ShapeSchema = z + .object({ + datasetId: z.string(), + guid: z.string(), + title: z.string(), + originalTitle: z.string().optional(), + type: z.string(), + data_type: z.string(), + calc_mode: datasetFieldCalcModeSchema, + }) + .merge(v12ClientOnlyFieldsSchema); + +// V12Tooltip schema +const v12TooltipSchema = z + .object({ + datasetId: z.string(), + guid: z.string(), + title: z.string(), + formatting: v12FormattingSchema.optional(), + data_type: z.string(), + calc_mode: datasetFieldCalcModeSchema, + }) + .merge(v12ClientOnlyFieldsSchema); + +// V12Label schema +const v12LabelSchema = z.object({ + datasetId: z.string(), + type: z.string(), + title: z.string(), + guid: z.string(), + formatting: v12FormattingSchema.optional(), + format: z.string().optional(), + data_type: z.string(), + calc_mode: datasetFieldCalcModeSchema, +}); + +// V12HierarchyField schema +const v12HierarchyFieldSchema = z.object({ + data_type: z.string(), + fields: z.array(v12FieldSchema), + type: z.string(), +}); + +// V12PointSizeConfig schema +const v12PointSizeConfigSchema = z.object({ + radius: z.number(), + minRadius: z.number(), + maxRadius: z.number(), +}); + +// V12ColorsConfig schema +const v12ColorsConfigSchema = z.object({ + thresholdsMode: z.string().optional(), + leftThreshold: z.string().optional(), + middleThreshold: z.string().optional(), + rightThreshold: z.string().optional(), + gradientPalette: z.string().optional(), + gradientMode: z.string().optional(), + polygonBorders: z.string().optional(), + reversed: z.boolean().optional(), + fieldGuid: z.string().optional(), + mountedColors: z.record(z.string(), z.string()).optional(), + coloredByMeasure: z.boolean().optional(), + palette: z.string().optional(), + colorMode: colorModeSchema.optional(), + nullMode: gradientNullModeSchema.optional(), +}); + +// V12ShapesConfig schema +const v12ShapesConfigSchema = z.object({ + mountedShapes: z.record(z.string(), z.string()).optional(), + fieldGuid: z.string().optional(), +}); + +// V12TooltipConfig schema +const v12TooltipConfigSchema = z.object({ + color: z.enum(colorFieldTitleList).optional(), + fieldTitle: z.enum(colorFieldTitleList).optional(), +}); + +// V12LayerSettings schema +const v12LayerSettingsSchema = z.object({ + id: z.string(), + name: z.string(), + type: z.string(), + alpha: z.number(), + valid: z.boolean(), +}); + +// V12CommonPlaceholders schema +const v12CommonPlaceholdersSchema = z.object({ + colors: z.array(v12ColorSchema), + labels: z.array(v12LabelSchema), + tooltips: z.array(v12TooltipSchema), + filters: z.array(v12FilterSchema), + sort: z.array(v12SortSchema), + shapes: z.array(v12ShapeSchema).optional(), + colorsConfig: v12ColorsConfigSchema.optional(), + geopointsConfig: v12PointSizeConfigSchema.optional(), + shapesConfig: v12ShapesConfigSchema.optional(), + tooltipConfig: v12TooltipConfigSchema.optional(), +}); + +// V12Layer schema +const v12LayerSchema = z.object({ + id: z.string(), + commonPlaceholders: v12CommonPlaceholdersSchema, + layerSettings: v12LayerSettingsSchema, + placeholders: z.array(v12PlaceholderSchema), +}); + +// V12Visualization schema +const v12VisualizationSchema = z.object({ + id: z.string(), + highchartsId: z.string().optional(), + selectedLayerId: z.string().optional(), + layers: z.array(v12LayerSchema).optional(), + placeholders: z.array(v12PlaceholderSchema), +}); + +// V12Update schema +const v12UpdateSchema = z.object({ + action: z.enum(v12UpdateActionsList), + field: z.any(), + debug_info: z.string().optional(), +}); + +// V12ChartsConfigDatasetField schema +const v12ChartsConfigDatasetFieldSchema = z.object({ + guid: z.string(), + title: z.string(), + calc_mode: datasetFieldCalcModeSchema.optional(), +}); + +// Main V12ChartsConfig schema +export const v12ChartsConfigSchema = z.object({ + title: z.string().optional(), + colors: z.array(v12ColorSchema), + colorsConfig: v12ColorsConfigSchema.optional(), + extraSettings: v12CommonSharedExtraSettingsSchema.optional(), + filters: z.array(v12FilterSchema), + geopointsConfig: v12PointSizeConfigSchema.optional(), + hierarchies: z.array(v12HierarchyFieldSchema), + labels: z.array(v12LabelSchema), + links: z.array(v12LinkSchema), + sort: z.array(v12SortSchema), + tooltips: z.array(v12TooltipSchema), + tooltipConfig: v12TooltipConfigSchema.optional(), + type: z.literal(DATALENS_TYPE_LITERAL), + updates: z.array(v12UpdateSchema), + visualization: v12VisualizationSchema, + shapes: z.array(v12ShapeSchema), + shapesConfig: v12ShapesConfigSchema.optional(), + version: z.literal(ChartsConfigVersion.V12), + datasetsIds: z.array(z.string()), + datasetsPartialFields: z.array(z.array(v12ChartsConfigDatasetFieldSchema)), + segments: z.array(v12FieldSchema), + chartType: z.string().optional(), +}); + +// Export JSON schema generation +export const chartApiValidationJsonSchema = z.toJSONSchema(v12ChartsConfigSchema); + +// Export the TypeScript type +export type V12ChartsConfigSchema = z.infer; + +// Export individual schemas for potential reuse +export { + v12ColorSchema, + v12ShapeSchema, + v12TooltipSchema, + v12LabelSchema, + v12FilterSchema, + v12SortSchema, + v12LinkSchema, + v12VisualizationSchema, + v12UpdateSchema, + v12FieldSchema, + v12HierarchyFieldSchema, + v12PointSizeConfigSchema, + v12ColorsConfigSchema, + v12ShapesConfigSchema, + v12TooltipConfigSchema, + v12CommonSharedExtraSettingsSchema, + v12NavigatorSettingsSchema, + v12FormattingSchema, + v12ClientOnlyFieldsSchema, + v12ChartsConfigDatasetFieldSchema, +}; From 74a35f30d4542f06a602058fb7e925fa0c6f8e28 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Mon, 15 Sep 2025 15:14:10 +0300 Subject: [PATCH 39/40] Revert "Remove not ready schemas" This reverts commit 20c64a681303c5479c71f2cabdf77276129f7d95. --- src/server/components/public-api/constants.ts | 107 ++++++++++++++++-- src/shared/schema/bi/schemas/datasets.ts | 6 +- src/shared/schema/mix/actions/dash.ts | 83 +++++++++++++- src/shared/schema/mix/actions/editor.ts | 28 ++++- src/shared/schema/mix/actions/wizard.ts | 76 ++++++++++++- src/shared/schema/mix/schemas/dash.ts | 49 ++++++++ src/shared/schema/mix/schemas/editor.ts | 47 ++++++++ src/shared/schema/mix/schemas/wizard.ts | 53 +++++++++ src/shared/schema/mix/types/dash.ts | 7 ++ src/shared/schema/mix/types/editor.ts | 5 + .../__tests__/dash-api.schema.test.ts | 4 +- .../zod-schemas/dash-api.schema.ts} | 3 +- .../zod-schemas/dataset-api.schema.ts} | 55 ++++++++- .../zod-schemas/wizard-chart-api.schema.ts} | 10 +- 14 files changed, 505 insertions(+), 28 deletions(-) create mode 100644 src/shared/schema/mix/types/editor.ts rename src/shared/{ => sdk}/zod-schemas/__tests__/dash-api.schema.test.ts (99%) rename src/shared/{zod-schemas/dash.ts => sdk/zod-schemas/dash-api.schema.ts} (99%) rename src/shared/{zod-schemas/dataset.ts => sdk/zod-schemas/dataset-api.schema.ts} (86%) rename src/shared/{zod-schemas/wizard.ts => sdk/zod-schemas/wizard-chart-api.schema.ts} (98%) diff --git a/src/server/components/public-api/constants.ts b/src/server/components/public-api/constants.ts index ebe99cde13..dd5256d467 100644 --- a/src/server/components/public-api/constants.ts +++ b/src/server/components/public-api/constants.ts @@ -9,6 +9,7 @@ export const PUBLIC_API_URL = '/rpc/:version/:action'; export const PUBLIC_API_ROUTE = `${PUBLIC_API_HTTP_METHOD} ${PUBLIC_API_URL}`; enum ApiTag { + Navigation = 'Navigation', Connection = 'Connection', Dataset = 'Dataset', Wizard = 'Wizard', @@ -18,7 +19,36 @@ enum ApiTag { export const PUBLIC_API_PROXY_MAP = { v0: { - // Connection + // navigation + // getNavigationList: { + // resolve: (api) => api.mix.getNavigationList, + // openApi: { + // summary: 'Get navigation list', + // tags: [ApiTag.Navigation], + // }, + // }, + // connection + // getConnection: { + // resolve: (api) => api.bi.getConnection, + // openApi: { + // summary: 'Get connection', + // tags: [ApiTag.Connection], + // }, + // }, + // updateConnection: { + // resolve: (api) => api.bi.updateConnection, + // openApi: { + // summary: 'Update connection', + // tags: [ApiTag.Connection], + // }, + // }, + // createConnection: { + // resolve: (api) => api.bi.createConnection, + // openApi: { + // summary: 'Create connection', + // tags: [ApiTag.Connection], + // }, + // }, deleteConnection: { resolve: (api) => api.bi.deleteConnection, openApi: { @@ -26,8 +56,7 @@ export const PUBLIC_API_PROXY_MAP = { tags: [ApiTag.Connection], }, }, - - // Dataset + // dataset getDataset: { resolve: (api) => api.bi.getDatasetByVersion, openApi: { @@ -56,8 +85,28 @@ export const PUBLIC_API_PROXY_MAP = { tags: [ApiTag.Dataset], }, }, - - // Wizard + // wizard + getWizardChart: { + resolve: (api) => api.mix.__getWizardChart__, + openApi: { + summary: 'Get wizard chart', + tags: [ApiTag.Wizard], + }, + }, + updateWizardChart: { + resolve: (api) => api.mix.__updateWizardChart__, + openApi: { + summary: 'Update wizard chart', + tags: [ApiTag.Wizard], + }, + }, + createWizardChart: { + resolve: (api) => api.mix.__createWizardChart__, + openApi: { + summary: 'Create wizard chart', + tags: [ApiTag.Wizard], + }, + }, deleteWizardChart: { resolve: (api) => api.mix.__deleteWizardChart__, openApi: { @@ -65,8 +114,28 @@ export const PUBLIC_API_PROXY_MAP = { tags: [ApiTag.Wizard], }, }, - - // Editor + // editor + getEditorChart: { + resolve: (api) => api.mix.__getEditorChart__, + openApi: { + summary: 'Get editor chart', + tags: [ApiTag.Editor], + }, + }, + // updateEditorChart: { + // resolve: (api) => api.mix.updateEditorChart, + // openApi: { + // summary: 'Update editor chart', + // tags: [ApiTag.Editor], + // }, + // }, + // createEditorChart: { + // resolve: (api) => api.mix.createEditorChart, + // openApi: { + // summary: 'Create editor chart', + // tags: [ApiTag.Editor], + // }, + // }, deleteEditorChart: { resolve: (api) => api.mix.__deleteEditorChart__, openApi: { @@ -74,8 +143,28 @@ export const PUBLIC_API_PROXY_MAP = { tags: [ApiTag.Editor], }, }, - - // Dashboard + // Dash + getDashboard: { + resolve: (api) => api.mix.__getDashboard__, + openApi: { + summary: 'Get dashboard', + tags: [ApiTag.Dashboard], + }, + }, + updateDashboard: { + resolve: (api) => api.mix.__updateDashboard__, + openApi: { + summary: 'Delete dashboard', + tags: [ApiTag.Dashboard], + }, + }, + createDashboard: { + resolve: (api) => api.mix.__createDashboard__, + openApi: { + summary: 'Create dashboard', + tags: [ApiTag.Dashboard], + }, + }, deleteDashboard: { resolve: (api) => api.mix.__deleteDashboard__, openApi: { diff --git a/src/shared/schema/bi/schemas/datasets.ts b/src/shared/schema/bi/schemas/datasets.ts index fcf0f7a4b2..3f9bcc5a9f 100644 --- a/src/shared/schema/bi/schemas/datasets.ts +++ b/src/shared/schema/bi/schemas/datasets.ts @@ -1,6 +1,10 @@ import z from 'zod/v4'; -import {datasetBodySchema, datasetOptionsSchema, datasetSchema} from '../../../zod-schemas/dataset'; +import { + datasetBodySchema, + datasetOptionsSchema, + datasetSchema, +} from '../../../sdk/zod-schemas/dataset-api.schema'; const createDatasetDefaultArgsSchema = z.object({ name: z.string(), diff --git a/src/shared/schema/mix/actions/dash.ts b/src/shared/schema/mix/actions/dash.ts index 2529664ac9..2ce4a2efec 100644 --- a/src/shared/schema/mix/actions/dash.ts +++ b/src/shared/schema/mix/actions/dash.ts @@ -1,6 +1,10 @@ +import _, {pick} from 'lodash'; import type {DeepNonNullable} from 'utility-types'; +import Dash from '../../../../server/components/sdk/dash'; +import {DASH_ENTRY_RELEVANT_FIELDS} from '../../../../server/constants'; import type {ChartsStats} from '../../../types/charts'; +import {EntryScope} from '../../../types/common'; import {createAction, createTypedAction} from '../../gateway-utils'; import {getTypedApi} from '../../simple-schema'; import {getEntryVisualizationType} from '../helpers'; @@ -11,19 +15,63 @@ import { prepareDatasetData, prepareWidgetDatasetData, } from '../helpers/dash'; -import {deleteDashArgsSchema, deleteDashResultSchema} from '../schemas/dash'; +import { + createDashArgsSchema, + createDashResultSchema, + deleteDashArgsSchema, + deleteDashResultSchema, + getDashArgsSchema, + getDashResultSchema, + updateDashArgsSchema, + updateDashResultSchema, +} from '../schemas/dash'; import type { CollectChartkitStatsArgs, CollectChartkitStatsResponse, CollectDashStatsArgs, CollectDashStatsResponse, + CreateDashResponse, GetEntriesDatasetsFieldsArgs, GetEntriesDatasetsFieldsResponse, GetWidgetsDatasetsFieldsArgs, GetWidgetsDatasetsFieldsResponse, + UpdateDashResponse, } from '../types'; export const dashActions = { + // WIP + __getDashboard__: createTypedAction( + { + paramsSchema: getDashArgsSchema, + resultSchema: getDashResultSchema, + }, + async (_, args, {headers, ctx}) => { + const {dashboardId, includePermissions, includeLinks, branch, revId} = args; + + if (!dashboardId || dashboardId === 'null') { + throw new Error(`Not found ${dashboardId} id`); + } + + const result = await Dash.read( + dashboardId, + { + includePermissions: includePermissions ? includePermissions?.toString() : '0', + includeLinks: includeLinks ? includeLinks?.toString() : '0', + ...(branch ? {branch} : {branch: 'published'}), + ...(revId ? {revId} : {}), + }, + headers, + ctx, + {forceMigrate: true}, + ); + + if (result.scope !== EntryScope.Dash) { + throw new Error('No entry found'); + } + + return pick(result, DASH_ENTRY_RELEVANT_FIELDS) as any; + }, + ), // WIP __deleteDashboard__: createTypedAction( { @@ -41,6 +89,39 @@ export const dashActions = { return {}; }, ), + // WIP + __updateDashboard__: createTypedAction( + { + paramsSchema: updateDashArgsSchema, + resultSchema: updateDashResultSchema, + }, + async (_, args, {headers, ctx}) => { + const {entryId} = args; + + const I18n = ctx.get('i18n'); + + return (await Dash.update(entryId as any, args as any, headers, ctx, I18n, { + forceMigrate: true, + })) as unknown as UpdateDashResponse; + }, + ), + // WIP + __createDashboard__: createTypedAction( + { + paramsSchema: createDashArgsSchema, + resultSchema: createDashResultSchema, + }, + async (_, args, {headers, ctx}) => { + const I18n = ctx.get('i18n'); + + return (await Dash.create( + args as any, + headers, + ctx, + I18n, + )) as unknown as CreateDashResponse; + }, + ), collectDashStats: createAction( async (_, args, {ctx}) => { diff --git a/src/shared/schema/mix/actions/editor.ts b/src/shared/schema/mix/actions/editor.ts index 7309bfba65..e5b0c7d4c3 100644 --- a/src/shared/schema/mix/actions/editor.ts +++ b/src/shared/schema/mix/actions/editor.ts @@ -9,9 +9,35 @@ import type { } from '../../us/types'; import {getEntryLinks} from '../helpers'; import {validateData} from '../helpers/editor/validation'; -import {deleteEditorChartArgsSchema, deleteEditorChartResultSchema} from '../schemas/editor'; +import { + deleteEditorChartArgsSchema, + deleteEditorChartResultSchema, + getEditorChartArgsSchema, + getEditorChartResultSchema, +} from '../schemas/editor'; +import type {GetEditorChartResponse} from '../types'; export const editorActions = { + // WIP + __getEditorChart__: createTypedAction( + { + paramsSchema: getEditorChartArgsSchema, + resultSchema: getEditorChartResultSchema, + }, + async (api, args) => { + const {includePermissions, includeLinks, revId, chardId, branch, workbookId} = args; + const typedApi = getTypedApi(api); + + return typedApi.us.getEntry({ + entryId: chardId, + includePermissionsInfo: includePermissions ? Boolean(includePermissions) : false, + includeLinks: includeLinks ? Boolean(includeLinks) : false, + ...(revId ? {revId} : {}), + workbookId: workbookId || null, + branch: branch || 'published', + }) as unknown as GetEditorChartResponse; + }, + ), createEditorChart: createAction( async (api, args, {ctx}) => { const {checkRequestForDeveloperModeAccess} = ctx.get('gateway'); diff --git a/src/shared/schema/mix/actions/wizard.ts b/src/shared/schema/mix/actions/wizard.ts index a6618be158..2d09c336f0 100644 --- a/src/shared/schema/mix/actions/wizard.ts +++ b/src/shared/schema/mix/actions/wizard.ts @@ -1,8 +1,82 @@ +import {USProvider} from '../../../../server/components/charts-engine/components/storage/united-storage/provider'; +import {EntryScope} from '../../../types'; import {createTypedAction} from '../../gateway-utils'; import {getTypedApi} from '../../simple-schema'; -import {deleteWizardChartArgsSchema, deleteWizardChartResultSchema} from '../schemas/wizard'; +import { + createWizardChartArgsSchema, + createWizardChartResultSchema, + deleteWizardChartArgsSchema, + deleteWizardChartResultSchema, + getWizardChartArgsSchema, + getWizardChartResultSchema, + updateWizardChartArgsSchema, + updateWizardChartResultSchema, +} from '../schemas/wizard'; export const wizardActions = { + // WIP + __getWizardChart__: createTypedAction( + { + paramsSchema: getWizardChartArgsSchema, + resultSchema: getWizardChartResultSchema, + }, + async (_, args, {ctx, headers}) => { + const {includePermissions, includeLinks, unreleased, revId, chardId} = args; + + const result = await USProvider.retrieveParsedWizardChart(ctx, { + id: chardId, + includePermissionsInfo: includePermissions ? includePermissions?.toString() : '0', + includeLinks: includeLinks ? includeLinks?.toString() : '0', + ...(revId ? {revId} : {}), + ...(unreleased ? {unreleased} : {unreleased: false}), + headers, + }); + + return result as any; + }, + ), + // WIP + __createWizardChart__: createTypedAction( + { + paramsSchema: createWizardChartArgsSchema, + resultSchema: createWizardChartResultSchema, + }, + async (_, args, {ctx, headers}) => { + const {data, type, key, workbookId, name} = args; + + const result = await USProvider.create(ctx, { + type, + data, + key, + name, + scope: EntryScope.Widget, + ...(workbookId ? {workbookId} : {workbookId: null}), + headers, + }); + + return result as any; + }, + ), + // WIP + __updateWizardChart__: createTypedAction( + { + paramsSchema: updateWizardChartArgsSchema, + resultSchema: updateWizardChartResultSchema, + }, + async (_, args, {ctx, headers}) => { + const {entryId, revId, data, type} = args; + + const result = await USProvider.update(ctx, { + entryId, + ...(revId ? {revId} : {}), + ...(type ? {type} : {}), + data, + headers, + }); + + return result as any; + }, + ), // WIP __deleteWizardChart__: createTypedAction( { diff --git a/src/shared/schema/mix/schemas/dash.ts b/src/shared/schema/mix/schemas/dash.ts index 74e3f8c236..640f32e70a 100644 --- a/src/shared/schema/mix/schemas/dash.ts +++ b/src/shared/schema/mix/schemas/dash.ts @@ -1,8 +1,57 @@ import z from 'zod/v4'; +import {EntryScope} from '../../..'; +import {dashSchema} from '../../../sdk/zod-schemas/dash-api.schema'; + export const deleteDashArgsSchema = z.object({ dashboardId: z.string(), lockToken: z.string().optional(), }); export const deleteDashResultSchema = z.object({}); + +const dashUsSchema = z.object({ + ...dashSchema.shape, + entryId: z.string(), + scope: z.literal(EntryScope.Dash), + public: z.boolean(), + isFavorite: z.boolean(), + createdAt: z.string(), + createdBy: z.string(), + updatedAt: z.string(), + updatedBy: z.string(), + revId: z.string(), + savedId: z.string(), + publishedId: z.string(), + meta: z.record(z.string(), z.string()), + links: z.record(z.string(), z.string()).optional(), + key: z.union([z.null(), z.string()]), + workbookId: z.union([z.null(), z.string()]), + type: z.literal(''), +}); + +export const updateDashArgsSchema = z.object({ + ...dashSchema.partial().shape, + entryId: z.string(), +}); + +export const updateDashResultSchema = dashUsSchema; + +export const createDashArgsSchema = z.object({ + ...dashSchema.shape, + workbookId: z.union([z.null(), z.string()]).optional(), + lockToken: z.string().optional(), + mode: z.literal(['publish', 'save']), +}); + +export const createDashResultSchema = dashUsSchema; + +export const getDashArgsSchema = z.object({ + dashboardId: z.string(), + revId: z.string().optional(), + includePermissions: z.boolean().optional().default(false), + includeLinks: z.boolean().optional().default(false), + branch: z.literal(['published', 'saved']).optional().default('published'), +}); + +export const getDashResultSchema = dashUsSchema; diff --git a/src/shared/schema/mix/schemas/editor.ts b/src/shared/schema/mix/schemas/editor.ts index 96f89fee9c..270031563e 100644 --- a/src/shared/schema/mix/schemas/editor.ts +++ b/src/shared/schema/mix/schemas/editor.ts @@ -1,7 +1,54 @@ import z from 'zod/v4'; +import {EDITOR_TYPE, EntryScope} from '../../..'; + export const deleteEditorChartArgsSchema = z.object({ chartId: z.string(), }); export const deleteEditorChartResultSchema = z.object({}); + +export const getEditorChartArgsSchema = z.object({ + chardId: z.string(), + workbookId: z.union([z.string(), z.null()]).default(null).optional(), + revId: z.string().optional(), + includePermissions: z.boolean().default(false).optional(), + includeLinks: z.boolean().default(false).optional(), + branch: z.literal(['saved', 'published']).default('published').optional(), +}); + +const editorUsSchema = z.object({ + data: z.union([ + z.object({ + js: z.string(), + url: z.string(), + params: z.string(), + shared: z.string(), + }), + z.object({ + controls: z.string(), + meta: z.string(), + params: z.string(), + prepare: z.string(), + sources: z.string(), + }), + ]), + entryId: z.string(), + scope: z.literal(EntryScope.Widget), + type: z.enum(EDITOR_TYPE), + public: z.boolean(), + isFavorite: z.boolean(), + createdAt: z.string(), + createdBy: z.string(), + updatedAt: z.string(), + updatedBy: z.string(), + revId: z.string(), + savedId: z.string(), + publishedId: z.string(), + meta: z.record(z.string(), z.string()), + links: z.record(z.string(), z.string()).optional(), + key: z.union([z.null(), z.string()]), + workbookId: z.union([z.null(), z.string()]), +}); + +export const getEditorChartResultSchema = editorUsSchema; diff --git a/src/shared/schema/mix/schemas/wizard.ts b/src/shared/schema/mix/schemas/wizard.ts index e1af087bdd..386ac0560b 100644 --- a/src/shared/schema/mix/schemas/wizard.ts +++ b/src/shared/schema/mix/schemas/wizard.ts @@ -1,5 +1,58 @@ import z from 'zod/v4'; +import {EntryScope, WizardType} from '../../..'; +import {v12ChartsConfigSchema} from '../../../sdk/zod-schemas/wizard-chart-api.schema'; + +export const getWizardChartArgsSchema = z.object({ + chardId: z.string(), + unreleased: z.boolean().default(false).optional(), + revId: z.string().optional(), + includePermissions: z.boolean().default(false).optional(), + includeLinks: z.boolean().default(false).optional(), +}); + +const wizardUsSchema = z.object({ + data: v12ChartsConfigSchema, + entryId: z.string(), + scope: z.literal(EntryScope.Widget), + type: z.enum(WizardType), + public: z.boolean(), + isFavorite: z.boolean(), + createdAt: z.string(), + createdBy: z.string(), + updatedAt: z.string(), + updatedBy: z.string(), + revId: z.string(), + savedId: z.string(), + publishedId: z.string(), + meta: z.record(z.string(), z.string()), + links: z.record(z.string(), z.string()).optional(), + key: z.union([z.null(), z.string()]), + workbookId: z.union([z.null(), z.string()]), +}); + +export const getWizardChartResultSchema = wizardUsSchema; + +export const createWizardChartArgsSchema = z.object({ + entryId: z.string(), + data: v12ChartsConfigSchema, + key: z.string(), + workbookId: z.union([z.string(), z.null()]).optional(), + type: z.enum(WizardType).optional(), + name: z.string(), +}); + +export const createWizardChartResultSchema = wizardUsSchema; + +export const updateWizardChartArgsSchema = z.object({ + entryId: z.string(), + revId: z.string().optional(), + data: v12ChartsConfigSchema, + type: z.enum(WizardType).optional(), +}); + +export const updateWizardChartResultSchema = wizardUsSchema; + export const deleteWizardChartArgsSchema = z.object({ chartId: z.string(), }); diff --git a/src/shared/schema/mix/types/dash.ts b/src/shared/schema/mix/types/dash.ts index 6a947623e5..649e267ad9 100644 --- a/src/shared/schema/mix/types/dash.ts +++ b/src/shared/schema/mix/types/dash.ts @@ -1,6 +1,9 @@ +import type z from 'zod/v4'; + import type {WizardVisualizationId} from '../../../constants'; import type {ChartsStats, DashStats, WorkbookId} from '../../../types'; import type {GetEntriesEntryResponse} from '../../us/types'; +import type {createDashResultSchema, updateDashResultSchema} from '../schemas/dash'; export type CollectDashStatsResponse = { status: string; @@ -51,3 +54,7 @@ export type GetWidgetsDatasetsFieldsArgs = { entriesIds: string[]; workbookId: WorkbookId; }; + +export type UpdateDashResponse = z.infer; + +export type CreateDashResponse = z.infer; diff --git a/src/shared/schema/mix/types/editor.ts b/src/shared/schema/mix/types/editor.ts new file mode 100644 index 0000000000..f645cc0149 --- /dev/null +++ b/src/shared/schema/mix/types/editor.ts @@ -0,0 +1,5 @@ +import type z from 'zod/v4'; + +import type {getEditorChartResultSchema} from '../schemas/editor'; + +export type GetEditorChartResponse = z.infer; diff --git a/src/shared/zod-schemas/__tests__/dash-api.schema.test.ts b/src/shared/sdk/zod-schemas/__tests__/dash-api.schema.test.ts similarity index 99% rename from src/shared/zod-schemas/__tests__/dash-api.schema.test.ts rename to src/shared/sdk/zod-schemas/__tests__/dash-api.schema.test.ts index 0db557d101..fccb1451a3 100644 --- a/src/shared/zod-schemas/__tests__/dash-api.schema.test.ts +++ b/src/shared/sdk/zod-schemas/__tests__/dash-api.schema.test.ts @@ -7,8 +7,8 @@ import { DashTabItemControlSourceType, DashTabItemTitleSizes, DashTabItemType, -} from '../..'; -import {type DashSchema, dashSchema} from '../dash'; +} from '../../..'; +import {type DashSchema, dashSchema} from '../dash-api.schema'; const DASH_DEFAULT_NAMESPACE = 'default'; diff --git a/src/shared/zod-schemas/dash.ts b/src/shared/sdk/zod-schemas/dash-api.schema.ts similarity index 99% rename from src/shared/zod-schemas/dash.ts rename to src/shared/sdk/zod-schemas/dash-api.schema.ts index d8dbcf77cc..f1918d1325 100644 --- a/src/shared/zod-schemas/dash.ts +++ b/src/shared/sdk/zod-schemas/dash-api.schema.ts @@ -9,8 +9,7 @@ import { DashTabItemControlSourceType, DashTabItemTitleSizes, DashTabItemType, -} from '..'; - +} from '../..'; const DASH_DEFAULT_NAMESPACE = 'default'; // Text definition diff --git a/src/shared/zod-schemas/dataset.ts b/src/shared/sdk/zod-schemas/dataset-api.schema.ts similarity index 86% rename from src/shared/zod-schemas/dataset.ts rename to src/shared/sdk/zod-schemas/dataset-api.schema.ts index f623bfb9a3..8623d4b787 100644 --- a/src/shared/zod-schemas/dataset.ts +++ b/src/shared/sdk/zod-schemas/dataset-api.schema.ts @@ -1,12 +1,12 @@ -import z from 'zod/v4'; +import * as z from 'zod/v4'; -import {ConnectorType} from '..'; +import {ConnectorType} from '../..'; import { DATASET_FIELD_TYPES, DATASET_VALUE_CONSTRAINT_TYPE, DatasetFieldAggregation, DatasetFieldType, -} from '../types/dataset'; +} from '../../types/dataset'; // Basic type schemas const parameterDefaultValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); @@ -71,6 +71,19 @@ const datasetFieldSchema = z.object({ value_constraint: datasetValueConstraintSchema.optional(), }); +// Dataset field error schema +const datasetFieldErrorSchema = z.object({ + guid: z.string(), + title: z.string(), + errors: z.array( + z.object({ + column: z.union([z.number(), z.null()]), + row: z.union([z.number(), z.null()]), + message: z.string(), + }), + ), +}); + // Dataset component error item schema const datasetComponentErrorItemSchema = z.object({ code: z.string(), @@ -247,7 +260,19 @@ const datasetOptionsSchema = z.object({ }), }); -const datasetBodySchema = z.object({ +// Dataset API error schema +const datasetApiErrorSchema = z.object({ + datasetId: z.string(), + error: z.object({ + code: z.string().optional(), + message: z.string().optional(), + }), +}); + +// Dataset selection map schema +const datasetSelectionMapSchema = z.record(z.string(), z.literal(true)); + +export const datasetBodySchema = z.object({ avatar_relations: z.array(datasetAvatarRelationSchema), component_errors: z.object({ items: z.array(datasetComponentErrorSchema), @@ -272,7 +297,7 @@ const datasetBodySchema = z.object({ }); // Main Dataset schema -const datasetSchema = z.object({ +export const datasetSchema = z.object({ id: z.string(), realName: z.string(), is_favorite: z.boolean(), @@ -296,4 +321,22 @@ const datasetSchema = z.object({ sources: z.array(datasetSourceSchema), }); -export {datasetBodySchema, datasetOptionsSchema, datasetSchema}; +// Export JSON schema generation +export const datasetApiValidationJsonSchema = z.toJSONSchema(datasetSchema); + +// Export the TypeScript type +export type DatasetApiValidationType = z.infer; + +// Export individual schemas for potential reuse +export { + datasetFieldSchema, + datasetSourceSchema, + datasetOptionsSchema, + datasetAvatarRelationSchema, + datasetSourceAvatarSchema, + obligatoryFilterSchema, + datasetComponentErrorSchema, + datasetFieldErrorSchema, + datasetApiErrorSchema, + datasetSelectionMapSchema, +}; diff --git a/src/shared/zod-schemas/wizard.ts b/src/shared/sdk/zod-schemas/wizard-chart-api.schema.ts similarity index 98% rename from src/shared/zod-schemas/wizard.ts rename to src/shared/sdk/zod-schemas/wizard-chart-api.schema.ts index e20f317c68..ea3370d78e 100644 --- a/src/shared/zod-schemas/wizard.ts +++ b/src/shared/sdk/zod-schemas/wizard-chart-api.schema.ts @@ -7,10 +7,10 @@ import { LabelsPositions, MapCenterMode, ZoomMode, -} from '..'; -import {WidgetSize} from '../constants'; -import {MARKUP_TYPE} from '../types/charts'; -import type {DatasetFieldCalcMode} from '../types/dataset'; +} from '../..'; +import {WidgetSize} from '../../constants'; +import {MARKUP_TYPE} from '../../types/charts'; +import type {DatasetFieldCalcMode} from '../../types/dataset'; import { AxisLabelFormatMode, AxisMode, @@ -18,7 +18,7 @@ import { ChartsConfigVersion, NumberFormatType, NumberFormatUnit, -} from '../types/wizard'; +} from '../../types/wizard'; // Helper type for enum to literal conversion type EnumToLiteral = T extends string From 3095744764e79baf7b83aca61c47746bdf7ff8e1 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Mon, 15 Sep 2025 15:19:53 +0300 Subject: [PATCH 40/40] Fixes --- src/server/components/public-api/constants.ts | 40 +++---------------- 1 file changed, 5 insertions(+), 35 deletions(-) diff --git a/src/server/components/public-api/constants.ts b/src/server/components/public-api/constants.ts index dd5256d467..15dd67df69 100644 --- a/src/server/components/public-api/constants.ts +++ b/src/server/components/public-api/constants.ts @@ -9,7 +9,6 @@ export const PUBLIC_API_URL = '/rpc/:version/:action'; export const PUBLIC_API_ROUTE = `${PUBLIC_API_HTTP_METHOD} ${PUBLIC_API_URL}`; enum ApiTag { - Navigation = 'Navigation', Connection = 'Connection', Dataset = 'Dataset', Wizard = 'Wizard', @@ -19,36 +18,7 @@ enum ApiTag { export const PUBLIC_API_PROXY_MAP = { v0: { - // navigation - // getNavigationList: { - // resolve: (api) => api.mix.getNavigationList, - // openApi: { - // summary: 'Get navigation list', - // tags: [ApiTag.Navigation], - // }, - // }, - // connection - // getConnection: { - // resolve: (api) => api.bi.getConnection, - // openApi: { - // summary: 'Get connection', - // tags: [ApiTag.Connection], - // }, - // }, - // updateConnection: { - // resolve: (api) => api.bi.updateConnection, - // openApi: { - // summary: 'Update connection', - // tags: [ApiTag.Connection], - // }, - // }, - // createConnection: { - // resolve: (api) => api.bi.createConnection, - // openApi: { - // summary: 'Create connection', - // tags: [ApiTag.Connection], - // }, - // }, + // Connection deleteConnection: { resolve: (api) => api.bi.deleteConnection, openApi: { @@ -56,7 +26,7 @@ export const PUBLIC_API_PROXY_MAP = { tags: [ApiTag.Connection], }, }, - // dataset + // Dataset getDataset: { resolve: (api) => api.bi.getDatasetByVersion, openApi: { @@ -85,7 +55,7 @@ export const PUBLIC_API_PROXY_MAP = { tags: [ApiTag.Dataset], }, }, - // wizard + // Wizard getWizardChart: { resolve: (api) => api.mix.__getWizardChart__, openApi: { @@ -114,7 +84,7 @@ export const PUBLIC_API_PROXY_MAP = { tags: [ApiTag.Wizard], }, }, - // editor + // Editor getEditorChart: { resolve: (api) => api.mix.__getEditorChart__, openApi: { @@ -143,7 +113,7 @@ export const PUBLIC_API_PROXY_MAP = { tags: [ApiTag.Editor], }, }, - // Dash + // Dashboard getDashboard: { resolve: (api) => api.mix.__getDashboard__, openApi: {