diff --git a/.storybook/vite.config.ts b/.storybook/vite.config.ts index f49622a49f..f6c6be4a0b 100644 --- a/.storybook/vite.config.ts +++ b/.storybook/vite.config.ts @@ -46,6 +46,9 @@ export default defineConfig({ apiClient: fileURLToPath( new URL('../redisinsight/api-client', import.meta.url), ), + 'riShared': fileURLToPath( + new URL('../redisinsight/api/src/ri-shared', import.meta.url), + ), }, }, server: { diff --git a/jest.config.cjs b/jest.config.cjs index c069331638..0b287b5c21 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -16,6 +16,7 @@ module.exports = { 'uiSrc/(.*)': '/redisinsight/ui/src/$1', '^apiClient$': '/redisinsight/api-client', 'apiClient/(.*)': '/redisinsight/api-client/$1', + '^riShared/(.*)$': '/redisinsight/api/src/ri-shared/$1', '@redislabsdev/redis-ui-components': '@redis-ui/components', '@redislabsdev/redis-ui-styles': '@redis-ui/styles', '@redislabsdev/redis-ui-icons': '@redis-ui/icons', diff --git a/redisinsight/api/src/common/utils/index.ts b/redisinsight/api/src/common/utils/index.ts index 58ade32415..148208f474 100644 --- a/redisinsight/api/src/common/utils/index.ts +++ b/redisinsight/api/src/common/utils/index.ts @@ -1,4 +1,4 @@ -export * from './array-index.helper'; +export * from '../../ri-shared/utils/array-index'; export * from './certificate-import.util'; export * from './errors.util'; export * from './merge.util'; diff --git a/redisinsight/api/src/common/validators/array-index.validator.ts b/redisinsight/api/src/common/validators/array-index.validator.ts index eee9404cc8..d97a422553 100644 --- a/redisinsight/api/src/common/validators/array-index.validator.ts +++ b/redisinsight/api/src/common/validators/array-index.validator.ts @@ -3,10 +3,7 @@ import { ValidatorConstraint, ValidatorConstraintInterface, } from 'class-validator'; -import { - ARRAY_INDEX_MAX, - parseArrayIndex, -} from 'src/common/utils/array-index.helper'; +import { ARRAY_INDEX_MAX, parseArrayIndex } from 'src/common/utils'; @ValidatorConstraint({ name: 'ArrayIndexValidator', async: false }) export class ArrayIndexValidator implements ValidatorConstraintInterface { diff --git a/redisinsight/api/src/ri-shared/README.md b/redisinsight/api/src/ri-shared/README.md new file mode 100644 index 0000000000..a273027fb9 --- /dev/null +++ b/redisinsight/api/src/ri-shared/README.md @@ -0,0 +1,33 @@ +# ri-shared — code shared across the RedisInsight apps (API + UI) + +Modules here are consumed by **both** the API (relative imports, e.g. +`src/ri-shared/utils/array-index`) and the UI (the `riShared/*` path alias). They live +inside `api/src` because the api's production build (`nest build` → `node dist/src/main`) +compiles with `api/src` as the tsc rootDir — sources outside it restructure `dist/` and +break the packaged app. The UI/Storybook/jest aliases simply point into this folder, so +shared code ships with zero extra packaging surface (desktop bundles it like any api file). + +## What belongs here + +- Cross-boundary **contracts**: value formats both sides must agree on (e.g. the + BigInt-as-string array index format), stable message formats, shared constants. +- Small **dependency-free** utilities needed verbatim on both sides. + +## Rules + +- **No imports** from Nest, React, ioredis, lodash or anything else — dependency-free + TypeScript only (the UI and API have separate node_modules; nothing here may assume + either). +- **es2019-compatible**: the api compiles this folder with `target: es2019` — no BigInt + literals (`1n` is TS2737; use `BigInt('...')`), no newer syntax. +- API code style (semicolons) — this folder is linted by `yarn lint:api`. +- Tests live next to the module (`*.spec.ts`, runs under the api jest config) and define + the shared behavior once — UI consumers exercise it through their barrel imports. + +## Alias wiring (when adding the alias to a new consumer) + +`redisinsight/ui/tsconfig.json` + `redisinsight/ui/vite.config.mjs` + +`redisinsight/ui/src/packages/vite.config.mjs` (Workbench plugin builds reach +the `uiSrc/utils` barrel) + `jest.config.cjs` (root, UI tests) + +`.storybook/vite.config.ts` + `redisinsight/desktop/tsconfig.json` all map +`riShared/*` → `redisinsight/api/src/ri-shared/*`. diff --git a/redisinsight/api/src/common/utils/array-index.helper.spec.ts b/redisinsight/api/src/ri-shared/utils/array-index.spec.ts similarity index 96% rename from redisinsight/api/src/common/utils/array-index.helper.spec.ts rename to redisinsight/api/src/ri-shared/utils/array-index.spec.ts index 88b4e322c4..1543f305d2 100644 --- a/redisinsight/api/src/common/utils/array-index.helper.spec.ts +++ b/redisinsight/api/src/ri-shared/utils/array-index.spec.ts @@ -2,9 +2,9 @@ import { ARRAY_INDEX_MAX, isValidArrayIndex, parseArrayIndex, -} from 'src/common/utils'; +} from './array-index'; -describe('array-index.helper', () => { +describe('shared array-index', () => { it('should expose max unsigned 64-bit value', () => { expect(ARRAY_INDEX_MAX).toEqual(BigInt('18446744073709551615')); }); diff --git a/redisinsight/api/src/common/utils/array-index.helper.ts b/redisinsight/api/src/ri-shared/utils/array-index.ts similarity index 63% rename from redisinsight/api/src/common/utils/array-index.helper.ts rename to redisinsight/api/src/ri-shared/utils/array-index.ts index f1a680d780..837b88b528 100644 --- a/redisinsight/api/src/common/utils/array-index.helper.ts +++ b/redisinsight/api/src/ri-shared/utils/array-index.ts @@ -1,13 +1,19 @@ /** * Redis array indexes are unsigned 64-bit integers (0 … 2^64−1) and exceed * Number.MAX_SAFE_INTEGER, so they travel as numeric strings end-to-end — - * never parseInt/Number, no JS-side arithmetic on indexes. + * never parseInt/Number, no JS-side arithmetic on indexes (the UI's Redux + * store keeps them as strings too). * - * Mirrored in redisinsight/ui/src/utils/arrayIndex.ts — keep semantics and - * tests in sync. + * Shared by the UI and the API (see src/ri-shared/README.md): the API + * imports it relatively (it lives in the api compile root, so `nest build` + * emits it into dist like any other api source), the UI through the + * `riShared/*` alias (wired in redisinsight/ui/vite.config.mjs, + * redisinsight/ui/tsconfig.json, .storybook/vite.config.ts and + * jest.config.cjs). It must stay dependency-free and es2019-compatible — + * BigInt('...') calls only, since BigInt literals are a syntax error + * (TS2737) under the api's es2019 target. */ -// 2^64 - 1; BigInt() call (not a literal) — this tsconfig targets es2019, -// where BigInt literals are a syntax error (TS2737). +// 2^64 - 1 export const ARRAY_INDEX_MAX = BigInt('18446744073709551615'); const ARRAY_INDEX_REGEX = /^\d+$/; diff --git a/redisinsight/desktop/tsconfig.json b/redisinsight/desktop/tsconfig.json index fe6781b1e5..5c0210ce73 100644 --- a/redisinsight/desktop/tsconfig.json +++ b/redisinsight/desktop/tsconfig.json @@ -29,7 +29,8 @@ "apiSrc/*": ["redisinsight/api/src/*"], "uiSrc/*": ["redisinsight/ui/src/*"], "apiClient": ["redisinsight/api-client"], - "apiClient/*": ["redisinsight/api-client/*"] + "apiClient/*": ["redisinsight/api-client/*"], + "riShared/*": ["redisinsight/api/src/ri-shared/*"] } }, "include": ["**/*"], diff --git a/redisinsight/ui/src/packages/vite.config.mjs b/redisinsight/ui/src/packages/vite.config.mjs index 0bddb6e354..2b6365110b 100644 --- a/redisinsight/ui/src/packages/vite.config.mjs +++ b/redisinsight/ui/src/packages/vite.config.mjs @@ -45,6 +45,9 @@ export default defineConfig({ '@redislabsdev/redis-ui-table': '@redis-ui/table', uiSrc: fileURLToPath(new URL('../../src', import.meta.url)), apiClient: fileURLToPath(new URL('../../../api-client', import.meta.url)), + riShared: fileURLToPath( + new URL('../../../api/src/ri-shared', import.meta.url), + ), }, dedupe: ['react', 'react-dom'], }, diff --git a/redisinsight/ui/src/utils/arrayIndex.ts b/redisinsight/ui/src/utils/arrayIndex.ts deleted file mode 100644 index e027875140..0000000000 --- a/redisinsight/ui/src/utils/arrayIndex.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Redis array indexes are unsigned 64-bit integers (0 … 2^64−1) and exceed - * Number.MAX_SAFE_INTEGER, so they stay numeric strings end-to-end — - * never parseInt/Number, no JS-side arithmetic on indexes; Redux stores - * them as strings. - * - * Mirrored in redisinsight/api/src/common/utils/array-index.helper.ts — - * keep semantics and tests in sync. - */ -// 2^64 - 1; BigInt() call form (not a literal) for parity with the API -// mirror, whose tsconfig targets es2019 (where BigInt literals are TS2737). -export const ARRAY_INDEX_MAX = BigInt('18446744073709551615') - -const ARRAY_INDEX_REGEX = /^\d+$/ - -// Max u64 is 20 digits; longer all-digit inputs can't be valid and a length -// guard keeps BigInt() from parsing arbitrarily large request payloads. -const ARRAY_INDEX_MAX_LENGTH = 20 - -/** - * Returns the canonical decimal string for a valid index ("007" → "7"), - * or null for anything else (empty or whitespace-only, negative, - * fractional, exponent, hex, > 2^64−1, non-string input). - */ -export const parseArrayIndex = (input: unknown): string | null => { - if (typeof input !== 'string') { - return null - } - - const value = input.trim() - if (!ARRAY_INDEX_REGEX.test(value)) { - return null - } - - if (value.length > ARRAY_INDEX_MAX_LENGTH) { - return null - } - - const index = BigInt(value) - return index > ARRAY_INDEX_MAX ? null : index.toString() -} - -export const isValidArrayIndex = (input: unknown): boolean => - parseArrayIndex(input) !== null diff --git a/redisinsight/ui/src/utils/index.ts b/redisinsight/ui/src/utils/index.ts index 79ddcfbdc6..d4ff6c0351 100644 --- a/redisinsight/ui/src/utils/index.ts +++ b/redisinsight/ui/src/utils/index.ts @@ -4,7 +4,7 @@ import RouterWithSubRoutes from './routerWithSubRoutes' export * from './common' export * from './validations' -export * from './arrayIndex' +export * from 'riShared/utils/array-index' export * from './statuses' export * from './instance' export * from './apiResponse' diff --git a/redisinsight/ui/src/utils/tests/arrayIndex.spec.ts b/redisinsight/ui/src/utils/tests/arrayIndex.spec.ts deleted file mode 100644 index 757f6bed85..0000000000 --- a/redisinsight/ui/src/utils/tests/arrayIndex.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { - ARRAY_INDEX_MAX, - isValidArrayIndex, - parseArrayIndex, -} from 'uiSrc/utils' - -describe('arrayIndex', () => { - it('should expose max unsigned 64-bit value', () => { - expect(ARRAY_INDEX_MAX).toEqual(BigInt('18446744073709551615')) - }) - - describe('parseArrayIndex', () => { - it.each([ - { input: '0', expected: '0' }, - { input: '7', expected: '7' }, - { input: '007', expected: '7' }, // leading zeros normalized - { input: ' 42 ', expected: '42' }, // outer whitespace trimmed - { input: '18446744073709551615', expected: '18446744073709551615' }, // max u64 - { input: '18446744073709551616', expected: null }, // max + 1 - { input: '184467440737095516150', expected: null }, // 21 digits — length guard - { input: '00000000000000000000042', expected: null }, // >20 chars — guard trumps normalization - { input: '-1', expected: null }, - { input: '1.5', expected: null }, - { input: '1e3', expected: null }, - { input: '0x10', expected: null }, - { input: 'abc', expected: null }, - { input: '1 2', expected: null }, // internal whitespace - { input: '', expected: null }, - { input: ' ', expected: null }, - ])('should return $expected for $input', ({ input, expected }) => { - expect(parseArrayIndex(input)).toEqual(expected) - }) - - it.each([null, undefined, 7, BigInt(7), {}])( - 'should return null for non-string %p', - (input) => { - expect(parseArrayIndex(input)).toEqual(null) - }, - ) - }) - - describe('isValidArrayIndex', () => { - it('should return true for a valid index', () => { - expect(isValidArrayIndex('123')).toEqual(true) - }) - it('should return false for an invalid index', () => { - expect(isValidArrayIndex('-1')).toEqual(false) - }) - it('should return false for non-string input', () => { - expect(isValidArrayIndex(42)).toEqual(false) - }) - }) -}) diff --git a/redisinsight/ui/tsconfig.json b/redisinsight/ui/tsconfig.json index bec43b37f5..bf750275ed 100644 --- a/redisinsight/ui/tsconfig.json +++ b/redisinsight/ui/tsconfig.json @@ -29,7 +29,8 @@ "paths": { "uiSrc/*": ["src/*"], "apiClient": ["../api-client"], - "apiClient/*": ["../api-client/*"] + "apiClient/*": ["../api-client/*"], + "riShared/*": ["../api/src/ri-shared/*"] } }, "include": [ diff --git a/redisinsight/ui/vite.config.mjs b/redisinsight/ui/vite.config.mjs index 03319c2e61..cf10d4f149 100644 --- a/redisinsight/ui/vite.config.mjs +++ b/redisinsight/ui/vite.config.mjs @@ -83,6 +83,7 @@ export default defineConfig({ '@redislabsdev/redis-ui-table': '@redis-ui/table', uiSrc: fileURLToPath(new URL('./src', import.meta.url)), apiClient: fileURLToPath(new URL('../api-client', import.meta.url)), + 'riShared': fileURLToPath(new URL('../api/src/ri-shared', import.meta.url)), }, }, server: {