diff --git a/.github/workflows/core-feature-alert.yml b/.github/workflows/core-feature-alert.yml index f7e9c2d62d..6de989ea33 100644 --- a/.github/workflows/core-feature-alert.yml +++ b/.github/workflows/core-feature-alert.yml @@ -19,7 +19,7 @@ jobs: run: echo "PR_CREATOR=${{ github.event.pull_request.user.login }}" >> $GITHUB_ENV - name: Post a notification about core feature changes if not already commented - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | const prCreator = process.env.PR_CREATOR; @@ -45,7 +45,7 @@ jobs: console.log('Comment already exists, skipping.'); } - name: Add reviewers to the PR if they are not the creator - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | const prCreator = process.env.PR_CREATOR; diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c0c44ec60..bc01f44185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,46 @@ All notable changes to this project will be documented in this file. +## [7.0.3] - 2025-06-17 + +## [7.0.2] - 2025-06-17 + +### Bug Fixes + +- Correct upgrade link ([#10138](https://github.com/Unleash/unleash/issues/10138)) + + +### Features + +- Report hostedBy and licenseType ([#10141](https://github.com/Unleash/unleash/issues/10141)) + +- Translate impact metrics to prom format ([#10147](https://github.com/Unleash/unleash/issues/10147)) + +- Expose impact metrics ([#10151](https://github.com/Unleash/unleash/issues/10151)) + + +### Miscellaneous Tasks + +- Clean up flag overview redesign ([#10140](https://github.com/Unleash/unleash/issues/10140)) + +- Remove flag enterprise-payg ([#10139](https://github.com/Unleash/unleash/issues/10139)) + +- Added flag for CR approver emails ([#10144](https://github.com/Unleash/unleash/issues/10144)) + +- Add PSF to approved licenses list ([#10148](https://github.com/Unleash/unleash/issues/10148)) + +- Now expose IFeatureUsageInfo to override telemetry checking ([#10149](https://github.com/Unleash/unleash/issues/10149)) + +- Improve json diff view ([#10146](https://github.com/Unleash/unleash/issues/10146)) + +- Use logger instead of console.error ([#10150](https://github.com/Unleash/unleash/issues/10150)) + + +### Refactor + +- Migrate from make-fetch-happen to ky and use ky natively ([#10134](https://github.com/Unleash/unleash/issues/10134)) + + ## [7.0.1] - 2025-06-13 ### Bug Fixes diff --git a/frontend/package.json b/frontend/package.json index 612a71672a..83d3c3165a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -64,10 +64,10 @@ "@types/react-router-dom": "5.3.3", "@types/react-table": "7.7.20", "@types/react-test-renderer": "18.3.1", - "@types/semver": "7.5.8", + "@types/semver": "7.7.0", "@types/uuid": "^9.0.0", - "@uiw/codemirror-theme-duotone": "4.23.10", - "@uiw/react-codemirror": "4.23.10", + "@uiw/codemirror-theme-duotone": "4.23.13", + "@uiw/react-codemirror": "4.23.13", "@unleash/proxy-client-react": "^5.0.0", "@vitejs/plugin-react": "4.3.4", "cartesian": "^1.0.1", @@ -114,7 +114,7 @@ "react-table": "7.8.0", "react-test-renderer": "18.3.1", "sass": "1.85.1", - "semver": "7.7.1", + "semver": "7.7.2", "swr": "2.3.3", "tss-react": "4.9.15", "typescript": "5.8.3", @@ -134,7 +134,7 @@ "jsonpath-plus": "10.3.0", "json5": "^2.2.2", "vite": "5.4.19", - "semver": "7.7.1", + "semver": "7.7.2", "ws": "^8.18.0", "@types/react": "18.3.18" }, diff --git a/frontend/src/component/common/NewConstraintAccordion/ConstraintsList/EditableConstraintsList.tsx b/frontend/src/component/common/NewConstraintAccordion/ConstraintsList/EditableConstraintsList.tsx index d4a5589c1b..0c37645762 100644 --- a/frontend/src/component/common/NewConstraintAccordion/ConstraintsList/EditableConstraintsList.tsx +++ b/frontend/src/component/common/NewConstraintAccordion/ConstraintsList/EditableConstraintsList.tsx @@ -1,5 +1,5 @@ import type React from 'react'; -import { useImperativeHandle } from 'react'; +import { useEffect, useImperativeHandle } from 'react'; import { forwardRef } from 'react'; import { styled } from '@mui/material'; import type { IConstraint } from 'interfaces/strategy'; @@ -9,6 +9,7 @@ import { ConstraintsList } from 'component/common/ConstraintsList/ConstraintsLis import { EditableConstraint } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/EditableConstraint'; import { createEmptyConstraint } from '../../../../utils/createEmptyConstraint.ts'; import { constraintId } from 'constants/constraintId.ts'; +import { v4 as uuidv4 } from 'uuid'; export interface IEditableConstraintsListRef { addConstraint?: (contextName: string) => void; } @@ -39,6 +40,17 @@ export const EditableConstraintsList = forwardRef< }, })); + useEffect(() => { + if (!constraints.every((constraint) => constraintId in constraint)) { + setConstraints( + constraints.map((constraint) => ({ + [constraintId]: uuidv4(), + ...constraint, + })), + ); + } + }, [constraints, setConstraints]); + const onDelete = (index: number) => { setConstraints( produce((draft) => { diff --git a/frontend/src/component/events/EventDiff/EventDiff.tsx b/frontend/src/component/events/EventDiff/EventDiff.tsx index c0b3c8dfd9..f5bc7bf8fd 100644 --- a/frontend/src/component/events/EventDiff/EventDiff.tsx +++ b/frontend/src/component/events/EventDiff/EventDiff.tsx @@ -23,6 +23,7 @@ interface IEventDiffProps { * @deprecated remove with flag improvedJsonDiff */ sort?: (a: IEventDiffResult, b: IEventDiffResult) => number; + excludeKeys?: string[]; } const DiffStyles = styled('div')(({ theme }) => ({ @@ -37,7 +38,6 @@ const DiffStyles = styled('div')(({ theme }) => ({ position: 'absolute', left: 0, top: 0, - marginLeft: '-10px', }, }, @@ -47,35 +47,65 @@ const DiffStyles = styled('div')(({ theme }) => ({ content: '"+"', }, }, + '.deletion': { color: theme.palette.eventLog.diffSub, '::before': { content: '"-"', }, }, + + '&[data-change-type="replacement"]': { + ':has(.addition)': { + color: theme.palette.eventLog.diffAdd, + }, + ':has(.deletion)': { + color: theme.palette.eventLog.diffSub, + }, + '.addition::before, .deletion::before': { + content: 'none', + }, + }, + + '.diff:not(:has(*))': { + '::before': { + content: '"(no changes)"', + }, + }, })); -const NewEventDiff: FC = ({ entry }) => { +const ButtonIcon = styled('span')(({ theme }) => ({ + marginInlineEnd: theme.spacing(0.5), +})); + +const NewEventDiff: FC = ({ entry, excludeKeys }) => { + const changeType = entry.preData && entry.data ? 'edit' : 'replacement'; + const showExpandButton = changeType === 'edit'; const [full, setFull] = useState(false); const diffId = useId(); return ( <> - - + {showExpandButton ? ( + + ) : null} + diff --git a/frontend/src/component/events/EventLog/useEventLogSearch.ts b/frontend/src/component/events/EventLog/useEventLogSearch.ts index b398c18335..86483c87b8 100644 --- a/frontend/src/component/events/EventLog/useEventLogSearch.ts +++ b/frontend/src/component/events/EventLog/useEventLogSearch.ts @@ -72,6 +72,7 @@ export const useEventLogSearch = ( createdBy: FilterItemParam, type: FilterItemParam, environment: FilterItemParam, + id: StringParam, ...extraParameters(logType), }; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/useEditableConstraint/editable-constraint-type.test.ts b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/useEditableConstraint/editable-constraint-type.test.ts new file mode 100644 index 0000000000..7eb54bdfaa --- /dev/null +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/useEditableConstraint/editable-constraint-type.test.ts @@ -0,0 +1,32 @@ +import { createEmptyConstraint } from 'utils/createEmptyConstraint'; +import { fromIConstraint, toIConstraint } from './editable-constraint-type.ts'; +import { constraintId } from 'constants/constraintId'; +import type { IConstraint } from 'interfaces/strategy.ts'; + +test('mapping to and from retains the constraint id', () => { + const constraint = createEmptyConstraint('context'); + + expect(toIConstraint(fromIConstraint(constraint))[constraintId]).toEqual( + constraint[constraintId], + ); +}); + +test('mapping to an editable constraint adds a constraint id if there is none', () => { + const constraint: IConstraint = createEmptyConstraint('context'); + delete constraint[constraintId]; + + const editableConstraint = fromIConstraint(constraint); + + expect(editableConstraint[constraintId]).toBeDefined(); + + const iConstraint = toIConstraint(editableConstraint); + expect(iConstraint[constraintId]).toEqual(editableConstraint[constraintId]); +}); + +test('mapping from an empty constraint removes redundant value / values', () => { + const constraint = { ...createEmptyConstraint('context'), value: '' }; + expect(constraint).toHaveProperty('value'); + + const transformed = toIConstraint(fromIConstraint(constraint)); + expect(transformed).not.toHaveProperty('value'); +}); diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/useEditableConstraint/editable-constraint-type.ts b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/useEditableConstraint/editable-constraint-type.ts index c55c546cfb..80c798ba85 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/useEditableConstraint/editable-constraint-type.ts +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/useEditableConstraint/editable-constraint-type.ts @@ -1,3 +1,4 @@ +import { constraintId } from 'constants/constraintId'; import { type DateOperator, isDateOperator, @@ -10,6 +11,7 @@ import { isSemVerOperator, } from 'constants/operators'; import type { IConstraint } from 'interfaces/strategy'; +import { v4 as uuidv4 } from 'uuid'; type EditableConstraintBase = Omit< IConstraint, @@ -72,12 +74,14 @@ export const fromIConstraint = ( const { value, values, operator, ...rest } = constraint; if (isSingleValueOperator(operator)) { return { + [constraintId]: uuidv4(), ...rest, operator, value: value ?? '', }; } else { return { + [constraintId]: uuidv4(), ...rest, operator, values: new Set(values), @@ -92,7 +96,7 @@ export const toIConstraint = (constraint: EditableConstraint): IConstraint => { const { values, ...rest } = constraint; return { ...rest, - values: Array.from(values), + values: Array.from(constraint.values), }; } }; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/useEditableConstraint/useEditableConstraint.test.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/useEditableConstraint/useEditableConstraint.test.tsx index fa2bab70f2..bdc671ea45 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/useEditableConstraint/useEditableConstraint.test.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/useEditableConstraint/useEditableConstraint.test.tsx @@ -40,11 +40,13 @@ test('calls onUpdate with new state', async () => { // gets called by useEffect, so we need to wait for the next render. await waitFor(() => { - expect(onUpdate).toHaveBeenCalledWith({ - contextName: 'context-field', - operator: IN, - values: [], - }); + expect(onUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + contextName: 'context-field', + operator: IN, + values: [], + }), + ); }); }); diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate.tsx index b37f23dbad..6e844ee14c 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate.tsx @@ -37,6 +37,7 @@ import { useDefaultStrategy } from '../../../project/Project/ProjectSettings/Pro import { FeatureStrategyForm } from '../FeatureStrategyForm/FeatureStrategyForm.tsx'; import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants'; import { Limit } from 'component/common/Limit/Limit'; +import { apiPayloadConstraintReplacer } from 'utils/api-payload-constraint-replacer.ts'; const useStrategyLimit = (strategyCount: number) => { const { uiConfig } = useUiConfig(); @@ -280,7 +281,7 @@ export const formatAddStrategyApiCode = ( } const url = `${unleashUrl}/api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies`; - const payload = JSON.stringify(strategy, undefined, 2); + const payload = JSON.stringify(strategy, apiPayloadConstraintReplacer, 2); return `curl --location --request POST '${url}' \\ --header 'Authorization: INSERT_API_KEY' \\ diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit.tsx index 9ddeaf5847..25e1fc33ef 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit.tsx @@ -35,6 +35,7 @@ import { getChangeRequestConflictCreatedDataFromScheduleData, } from './change-request-conflict-data.ts'; import { constraintId } from 'constants/constraintId.ts'; +import { apiPayloadConstraintReplacer } from 'utils/api-payload-constraint-replacer.ts'; const useTitleTracking = () => { const [previousTitle, setPreviousTitle] = useState(''); @@ -352,7 +353,11 @@ export const formatUpdateStrategyApiCode = ( }; const url = `${unleashUrl}/api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies/${strategyId}`; - const payload = JSON.stringify(sortedStrategy, undefined, 2); + const payload = JSON.stringify( + sortedStrategy, + apiPayloadConstraintReplacer, + 2, + ); return `curl --location --request PUT '${url}' \\ --header 'Authorization: INSERT_API_KEY' \\ diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx index 494cb1ce23..40a1dcf1e9 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx @@ -143,28 +143,6 @@ const StyledAlertBox = styled(Box)(({ theme }) => ({ }, })); -const StyledEnvironmentBox = styled(Box)(({ theme }) => ({ - display: 'flex', - alignItems: 'center', -})); - -const EnvironmentIconBox = styled(Box)(({ theme }) => ({ - transform: 'scale(0.9)', - display: 'flex', - alignItems: 'center', -})); - -const EnvironmentTypography = styled(Typography, { - shouldForwardProp: (prop) => prop !== 'enabled', -})<{ enabled: boolean }>(({ enabled }) => ({ - fontWeight: enabled ? 'bold' : 'normal', -})); - -const EnvironmentTypographyHeader = styled(Typography)(({ theme }) => ({ - marginRight: theme.spacing(0.5), - color: theme.palette.text.secondary, -})); - const StyledTab = styled(Tab)(({ theme }) => ({ width: '100px', })); @@ -173,11 +151,11 @@ const StyledBadge = styled(Badge)(({ theme }) => ({ marginLeft: theme.spacing(1), })); -const StyledConstraintSeparator = styled(ConstraintSeparator)(({ theme }) => ({ +const StyledConstraintSeparator = styled(ConstraintSeparator)({ top: '-10px', left: '0', transform: 'translateY(0)', -})); +}); export const FeatureStrategyForm = ({ projectId, diff --git a/frontend/src/component/project/Project/Project.styles.ts b/frontend/src/component/project/Project/Project.styles.ts index 95265cd284..fc5bc927fa 100644 --- a/frontend/src/component/project/Project/Project.styles.ts +++ b/frontend/src/component/project/Project/Project.styles.ts @@ -59,6 +59,7 @@ export const StyledProjectTitle = styled('h1')(({ theme }) => ({ alignItems: 'center', gap: theme.spacing(2), overflow: 'hidden', + lineHeight: 1.5, })); export const StyledSeparator = styled('div')(({ theme }) => ({ diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions.tsx index 5f9bc5bbc8..75e58e01f8 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions.tsx @@ -9,7 +9,6 @@ import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { BulkDisableDialog } from 'component/feature/FeatureToggleList/BulkDisableDialog'; import { BulkEnableDialog } from 'component/feature/FeatureToggleList/BulkEnableDialog'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; interface IProjectFeaturesBatchActionsProps { selectedIds: string[]; @@ -72,32 +71,20 @@ export const ProjectFeaturesBatchActions: FC< return ( <> - setShowBulkEnableDialog(true)} - > - Enable - - } - /> - setShowBulkDisableDialog(true)} - > - Disable - - } - /> + + { return `curl --location --request POST '${uiConfig.unleashUrl}/api/admin/segments' \\ --header 'Authorization: INSERT_API_KEY' \\ --header 'Content-Type: application/json' \\ ---data-raw '${JSON.stringify(getSegmentPayload(), undefined, 2)}'`; +--data-raw '${JSON.stringify(getSegmentPayload(), apiPayloadConstraintReplacer, 2)}'`; }; const handleSubmit = async (e: React.FormEvent) => { diff --git a/frontend/src/component/segments/EditSegment/EditSegment.tsx b/frontend/src/component/segments/EditSegment/EditSegment.tsx index b385177d2e..1a9d67cf0f 100644 --- a/frontend/src/component/segments/EditSegment/EditSegment.tsx +++ b/frontend/src/component/segments/EditSegment/EditSegment.tsx @@ -24,15 +24,32 @@ import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentL import { useOptionalPathParam } from 'hooks/useOptionalPathParam'; import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi'; import { useHighestPermissionChangeRequestEnvironment } from 'hooks/useHighestPermissionChangeRequestEnvironment'; +import type { ISegment } from 'interfaces/segment.ts'; +import { constraintId } from 'constants/constraintId.ts'; +import { v4 as uuidv4 } from 'uuid'; +import { apiPayloadConstraintReplacer } from 'utils/api-payload-constraint-replacer.ts'; interface IEditSegmentProps { modal?: boolean; } +const addIdSymbolToConstraints = (segment?: ISegment): ISegment | undefined => { + if (!segment) return; + + const constraints = segment.constraints.map((constraint) => { + return { ...constraint, [constraintId]: uuidv4() }; + }); + + return { ...segment, constraints }; +}; + export const EditSegment = ({ modal }: IEditSegmentProps) => { const projectId = useOptionalPathParam('projectId'); const segmentId = useRequiredPathParam('segmentId'); - const { segment } = useSegment(Number(segmentId)); + const { segment: segmentWithoutConstraintIds } = useSegment( + Number(segmentId), + ); + const segment = addIdSymbolToConstraints(segmentWithoutConstraintIds); const { uiConfig } = useUiConfig(); const { setToastData, setToastApiError } = useToast(); const navigate = useNavigate(); @@ -72,7 +89,7 @@ export const EditSegment = ({ modal }: IEditSegmentProps) => { }/api/admin/segments/${segmentId}' \\ --header 'Authorization: INSERT_API_KEY' \\ --header 'Content-Type: application/json' \\ ---data-raw '${JSON.stringify(getSegmentPayload(), undefined, 2)}'`; +--data-raw '${JSON.stringify(getSegmentPayload(), apiPayloadConstraintReplacer, 2)}'`; }; const highestPermissionChangeRequestEnv = diff --git a/frontend/src/component/segments/SegmentForm.tsx b/frontend/src/component/segments/SegmentForm.tsx index 8a1c38c7ad..0d3cfcee14 100644 --- a/frontend/src/component/segments/SegmentForm.tsx +++ b/frontend/src/component/segments/SegmentForm.tsx @@ -1,4 +1,4 @@ -import type { IConstraint } from 'interfaces/strategy'; +import type { IConstraint, IConstraintWithId } from 'interfaces/strategy'; import { SegmentFormStepOne } from './SegmentFormStepOne.tsx'; import { SegmentFormStepTwo } from './SegmentFormStepTwo.tsx'; import type React from 'react'; @@ -14,7 +14,7 @@ interface ISegmentProps { name: string; description: string; project?: string; - constraints: IConstraint[]; + constraints: IConstraintWithId[]; setName: React.Dispatch>; setDescription: React.Dispatch>; setProject: React.Dispatch>; diff --git a/frontend/src/component/segments/SegmentFormStepTwo.test.tsx b/frontend/src/component/segments/SegmentFormStepTwo.test.tsx index 84dcd21bf1..6a8efec383 100644 --- a/frontend/src/component/segments/SegmentFormStepTwo.test.tsx +++ b/frontend/src/component/segments/SegmentFormStepTwo.test.tsx @@ -3,12 +3,12 @@ import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { testServerRoute, testServerSetup } from 'utils/testServer'; import { SegmentFormStepTwo } from './SegmentFormStepTwo.tsx'; -import type { IConstraint } from 'interfaces/strategy'; import { vi } from 'vitest'; import { CREATE_SEGMENT, UPDATE_PROJECT_SEGMENT, } from 'component/providers/AccessProvider/permissions'; +import type { IConstraintWithId } from 'interfaces/strategy.ts'; const server = testServerSetup(); @@ -26,7 +26,7 @@ const setupRoutes = () => { const defaultProps = { project: undefined, - constraints: [] as IConstraint[], + constraints: [] as IConstraintWithId[], setConstraints: vi.fn(), setCurrentStep: vi.fn(), mode: 'create' as const, diff --git a/frontend/src/component/segments/SegmentFormStepTwo.tsx b/frontend/src/component/segments/SegmentFormStepTwo.tsx index ddb84cf021..abea3aeb9e 100644 --- a/frontend/src/component/segments/SegmentFormStepTwo.tsx +++ b/frontend/src/component/segments/SegmentFormStepTwo.tsx @@ -13,7 +13,7 @@ import { UPDATE_SEGMENT, } from 'component/providers/AccessProvider/permissions'; import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext'; -import type { IConstraint } from 'interfaces/strategy'; +import type { IConstraint, IConstraintWithId } from 'interfaces/strategy'; import { useNavigate } from 'react-router-dom'; import { EditableConstraintsList } from 'component/common/NewConstraintAccordion/ConstraintsList/EditableConstraintsList'; import type { IEditableConstraintsListRef } from 'component/common/NewConstraintAccordion/ConstraintsList/EditableConstraintsList'; @@ -33,7 +33,7 @@ import { GO_BACK } from 'constants/navigate'; interface ISegmentFormPartTwoProps { project?: string; - constraints: IConstraint[]; + constraints: IConstraintWithId[]; setConstraints: React.Dispatch>; setCurrentStep: React.Dispatch>; mode: SegmentFormMode; diff --git a/frontend/src/component/segments/hooks/useSegmentForm.ts b/frontend/src/component/segments/hooks/useSegmentForm.ts index 09e106753e..951619715d 100644 --- a/frontend/src/component/segments/hooks/useSegmentForm.ts +++ b/frontend/src/component/segments/hooks/useSegmentForm.ts @@ -1,6 +1,8 @@ -import type { IConstraint } from 'interfaces/strategy'; +import type { IConstraint, IConstraintWithId } from 'interfaces/strategy'; import { useEffect, useState } from 'react'; import { useSegmentValidation } from 'hooks/api/getters/useSegmentValidation/useSegmentValidation'; +import { v4 as uuidv4 } from 'uuid'; +import { constraintId } from 'constants/constraintId'; export const useSegmentForm = ( initialName = '', @@ -11,8 +13,13 @@ export const useSegmentForm = ( const [name, setName] = useState(initialName); const [description, setDescription] = useState(initialDescription); const [project, setProject] = useState(initialProject); - const [constraints, setConstraints] = - useState(initialConstraints); + const initialConstraintsWithId = initialConstraints.map((constraint) => ({ + [constraintId]: uuidv4(), + ...constraint, + })); + const [constraints, setConstraints] = useState( + initialConstraintsWithId, + ); const [errors, setErrors] = useState({}); const nameError = useSegmentValidation(name, initialName); @@ -29,7 +36,7 @@ export const useSegmentForm = ( }, [initialProject]); useEffect(() => { - setConstraints(initialConstraints); + setConstraints(initialConstraintsWithId); // eslint-disable-next-line }, [JSON.stringify(initialConstraints)]); @@ -61,7 +68,9 @@ export const useSegmentForm = ( project, setProject, constraints, - setConstraints, + setConstraints: setConstraints as React.Dispatch< + React.SetStateAction + >, getSegmentPayload, clearErrors, errors, diff --git a/frontend/src/interfaces/strategy.ts b/frontend/src/interfaces/strategy.ts index b2aa226009..0f46621f79 100644 --- a/frontend/src/interfaces/strategy.ts +++ b/frontend/src/interfaces/strategy.ts @@ -68,6 +68,10 @@ export interface IConstraint { [constraintId]?: string; } +export interface IConstraintWithId extends IConstraint { + [constraintId]: string; +} + export interface IFeatureStrategySortOrder { id: string; sortOrder: number; diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index fc4bc59793..d9d25327d6 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -59,7 +59,6 @@ export type UiFlags = { personalAccessTokensKillSwitch?: boolean; demo?: boolean; googleAuthEnabled?: boolean; - disableBulkToggle?: boolean; advancedPlayground?: boolean; strategyVariant?: boolean; doraMetrics?: boolean; diff --git a/frontend/src/utils/api-payload-constraint-replacer.test.ts b/frontend/src/utils/api-payload-constraint-replacer.test.ts new file mode 100644 index 0000000000..1790d2b6bd --- /dev/null +++ b/frontend/src/utils/api-payload-constraint-replacer.test.ts @@ -0,0 +1,57 @@ +import type { IConstraint } from 'interfaces/strategy'; +import { serializeConstraint } from './api-payload-constraint-replacer.ts'; +import { constraintId } from 'constants/constraintId'; + +test('keys are ordered in the expected order', () => { + const input: IConstraint = { + values: ['something'], + inverted: true, + operator: 'STR_CONTAINS', + contextName: 'context', + caseInsensitive: true, + }; + + const output = serializeConstraint(input); + + expect(Object.entries(output)).toStrictEqual([ + ['contextName', 'context'], + ['operator', 'STR_CONTAINS'], + ['values', ['something']], + ['caseInsensitive', true], + ['inverted', true], + ]); +}); + +test('only value OR values is present, not both', () => { + const input: IConstraint = { + value: 'something', + values: ['something else'], + inverted: true, + operator: 'IN', + contextName: 'context', + caseInsensitive: true, + }; + + const noValue = serializeConstraint(input); + expect(noValue.values).toStrictEqual(['something else']); + expect(noValue).not.toHaveProperty('value'); + + const noValues = serializeConstraint({ + ...input, + operator: 'SEMVER_EQ', + }); + expect(noValues.value).toStrictEqual('something'); + expect(noValues).not.toHaveProperty('values'); +}); + +test('constraint id is not included', () => { + const input: IConstraint = { + [constraintId]: 'constraint-id', + value: 'something', + operator: 'IN', + contextName: 'context', + }; + + const output = serializeConstraint(input); + expect(constraintId in output).toBeFalsy(); +}); diff --git a/frontend/src/utils/api-payload-constraint-replacer.ts b/frontend/src/utils/api-payload-constraint-replacer.ts new file mode 100644 index 0000000000..244b207d74 --- /dev/null +++ b/frontend/src/utils/api-payload-constraint-replacer.ts @@ -0,0 +1,39 @@ +import { isSingleValueOperator } from 'constants/operators'; +import type { IConstraint } from 'interfaces/strategy'; + +export const serializeConstraint = ({ + value, + values, + inverted, + operator, + contextName, + caseInsensitive, +}: IConstraint): IConstraint => { + const makeConstraint = ( + valueProp: { value: string } | { values: string[] }, + ): IConstraint => { + return { + contextName, + operator, + ...valueProp, + caseInsensitive, + inverted, + }; + }; + + if (isSingleValueOperator(operator)) { + return makeConstraint({ value: value || '' }); + } + + return makeConstraint({ values: values || [] }); +}; + +export const apiPayloadConstraintReplacer = (key: string, value: any) => { + if (key !== 'constraints' || !Array.isArray(value)) { + return value; + } + const orderedConstraints = (value as IConstraint[]).map( + serializeConstraint, + ); + return orderedConstraints; +}; diff --git a/frontend/src/utils/createEmptyConstraint.ts b/frontend/src/utils/createEmptyConstraint.ts index 8556812b46..49e2887cbf 100644 --- a/frontend/src/utils/createEmptyConstraint.ts +++ b/frontend/src/utils/createEmptyConstraint.ts @@ -1,16 +1,15 @@ import { constraintId } from 'constants/constraintId'; -import { dateOperators } from 'constants/operators'; -import type { IConstraint } from 'interfaces/strategy'; -import { oneOf } from 'utils/oneOf'; +import { isDateOperator } from 'constants/operators'; +import type { IConstraintWithId } from 'interfaces/strategy'; import { operatorsForContext } from 'utils/operatorsForContext'; import { v4 as uuidv4 } from 'uuid'; -export const createEmptyConstraint = (contextName: string): IConstraint => { +export const createEmptyConstraint = ( + contextName: string, +): IConstraintWithId => { const operator = operatorsForContext(contextName)[0]; - const value = oneOf(dateOperators, operator) - ? new Date().toISOString() - : ''; + const value = isDateOperator(operator) ? new Date().toISOString() : ''; return { contextName, diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 90cedd256e..a13931cd03 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2877,14 +2877,14 @@ __metadata: linkType: hard "@tanstack/react-virtual@npm:^3.11.3": - version: 3.13.9 - resolution: "@tanstack/react-virtual@npm:3.13.9" + version: 3.13.10 + resolution: "@tanstack/react-virtual@npm:3.13.10" dependencies: - "@tanstack/virtual-core": "npm:3.13.9" + "@tanstack/virtual-core": "npm:3.13.10" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10c0/aa05fb24e30686516e74ccdec94a83d195615a4f29bc866a53ee6b0107e466c6d6e3e947b3fb0613b907b0f982d74b00367868cf6b1ac956562d0e7c24d6764b + checksum: 10c0/587ef4db703cc9d870ee68b3f1471118fe69920e8f59cf627a359d518331c6684069fb3e159cd35ac458ea60a829b66953b774314833e1cb10b365255791559e languageName: node linkType: hard @@ -2895,10 +2895,10 @@ __metadata: languageName: node linkType: hard -"@tanstack/virtual-core@npm:3.13.9": - version: 3.13.9 - resolution: "@tanstack/virtual-core@npm:3.13.9" - checksum: 10c0/6e9526a9d52f8ddc54af8a1dc7380814b10ab38d8a4265e362a5b69c3132097ace51496d4206fe8aa90e33129aaf1a177c93d7ed018b5564b78e057fc9cdb48d +"@tanstack/virtual-core@npm:3.13.10": + version: 3.13.10 + resolution: "@tanstack/virtual-core@npm:3.13.10" + checksum: 10c0/ecfe56cc37db088416abb1f1b9641cc7b05b387bb532e4fe42f30a5477e55fdbb724f54e8dc5c3d8b380a5e9f80cc11426519825f8ecbcb6ca717639ba702cc8 languageName: node linkType: hard @@ -3199,20 +3199,20 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 22.13.10 - resolution: "@types/node@npm:22.13.10" + version: 24.0.3 + resolution: "@types/node@npm:24.0.3" dependencies: - undici-types: "npm:~6.20.0" - checksum: 10c0/a3865f9503d6f718002374f7b87efaadfae62faa499c1a33b12c527cfb9fd86f733e1a1b026b80c5a0e4a965701174bc3305595a7d36078aa1abcf09daa5dee9 + undici-types: "npm:~7.8.0" + checksum: 10c0/9c3c4e87600d1cf11e291c2fd4bfd806a615455463c30a0ef6dc9c801b3423344d9b82b8084e3ccabce485a7421ebb61a66e9676181bd7d9aea4759998a120d5 languageName: node linkType: hard "@types/node@npm:^22.0.0": - version: 22.15.18 - resolution: "@types/node@npm:22.15.18" + version: 22.15.32 + resolution: "@types/node@npm:22.15.32" dependencies: undici-types: "npm:~6.21.0" - checksum: 10c0/e23178c568e2dc6b93b6aa3b8dfb45f9556e527918c947fe7406a4c92d2184c7396558912400c3b1b8d0fa952ec63819aca2b8e4d3545455fc6f1e9623e09ca6 + checksum: 10c0/63a2fa52adf1134d1b3bee8b1862d4b8e4550fffc190551068d3d41a41d9e5c0c8f1cb81faa18767b260637360f662115c26c5e4e7718868ead40c4a57cbc0e3 languageName: node linkType: hard @@ -3313,10 +3313,10 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:7.5.8": - version: 7.5.8 - resolution: "@types/semver@npm:7.5.8" - checksum: 10c0/8663ff927234d1c5fcc04b33062cb2b9fcfbe0f5f351ed26c4d1e1581657deebd506b41ff7fdf89e787e3d33ce05854bc01686379b89e9c49b564c4cfa988efa +"@types/semver@npm:7.7.0": + version: 7.7.0 + resolution: "@types/semver@npm:7.7.0" + checksum: 10c0/6b5f65f647474338abbd6ee91a6bbab434662ddb8fe39464edcbcfc96484d388baad9eb506dff217b6fc1727a88894930eb1f308617161ac0f376fe06be4e1ee languageName: node linkType: hard @@ -3408,9 +3408,9 @@ __metadata: languageName: node linkType: hard -"@uiw/codemirror-extensions-basic-setup@npm:4.23.10": - version: 4.23.10 - resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.23.10" +"@uiw/codemirror-extensions-basic-setup@npm:4.23.13": + version: 4.23.13 + resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.23.13" dependencies: "@codemirror/autocomplete": "npm:^6.0.0" "@codemirror/commands": "npm:^6.0.0" @@ -3427,22 +3427,22 @@ __metadata: "@codemirror/search": ">=6.0.0" "@codemirror/state": ">=6.0.0" "@codemirror/view": ">=6.0.0" - checksum: 10c0/64c233857b1bf878bf630297b1d4b3df14e13761ff38ceccf7a1fa21d521be288311333b7cbff927889f9a899848f4fccefd975ea5fa8d626ea4aef088f13ee8 + checksum: 10c0/db0d1c60c8b13f69aa02b969618d3b9f5aafab23af3c8d9be9a88016aff94de1e45a1c850daf7740d265a3e5452a916112f73e5c22857346a7002c243c934159 languageName: node linkType: hard -"@uiw/codemirror-theme-duotone@npm:4.23.10": - version: 4.23.10 - resolution: "@uiw/codemirror-theme-duotone@npm:4.23.10" +"@uiw/codemirror-theme-duotone@npm:4.23.13": + version: 4.23.13 + resolution: "@uiw/codemirror-theme-duotone@npm:4.23.13" dependencies: - "@uiw/codemirror-themes": "npm:4.23.10" - checksum: 10c0/058fe2ec927fb7d9fdd7a040c1fa52ed52b2b786e2917ae981a687b1aafeac26022900aac625c2018288c9bd7ab9e056854d6628eaaea46a4251b0022693df7c + "@uiw/codemirror-themes": "npm:4.23.13" + checksum: 10c0/3c0994d7731fdad1537d66f331b3bc1f4308e9388001b3a1d168c8e4b751f28661652533b0c830b3f30456132572686a298af0672f3f36872a08fdc1cf9b1e47 languageName: node linkType: hard -"@uiw/codemirror-themes@npm:4.23.10": - version: 4.23.10 - resolution: "@uiw/codemirror-themes@npm:4.23.10" +"@uiw/codemirror-themes@npm:4.23.13": + version: 4.23.13 + resolution: "@uiw/codemirror-themes@npm:4.23.13" dependencies: "@codemirror/language": "npm:^6.0.0" "@codemirror/state": "npm:^6.0.0" @@ -3451,19 +3451,19 @@ __metadata: "@codemirror/language": ">=6.0.0" "@codemirror/state": ">=6.0.0" "@codemirror/view": ">=6.0.0" - checksum: 10c0/29f980789a535ae021ca8d8bec3ecee4dda8cb11c5451729c04481310cc04cd7c00ddb3011137f5cc305d565bc0a3464ebd88fdde9359e296d8f0a6cf6477811 + checksum: 10c0/4e3bc3f12681727d41ae3c60c17fab5be267cb2675ace8ce6c56c66d6453715bf6d00c9b356d27df70f2104b0eaeda8c734faf2f05b38af05e7c5b2306f2a182 languageName: node linkType: hard -"@uiw/react-codemirror@npm:4.23.10": - version: 4.23.10 - resolution: "@uiw/react-codemirror@npm:4.23.10" +"@uiw/react-codemirror@npm:4.23.13": + version: 4.23.13 + resolution: "@uiw/react-codemirror@npm:4.23.13" dependencies: "@babel/runtime": "npm:^7.18.6" "@codemirror/commands": "npm:^6.1.0" "@codemirror/state": "npm:^6.1.1" "@codemirror/theme-one-dark": "npm:^6.0.0" - "@uiw/codemirror-extensions-basic-setup": "npm:4.23.10" + "@uiw/codemirror-extensions-basic-setup": "npm:4.23.13" codemirror: "npm:^6.0.0" peerDependencies: "@babel/runtime": ">=7.11.0" @@ -3473,7 +3473,7 @@ __metadata: codemirror: ">=6.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 10c0/cbcb928c84df2a6a03fd7789a3db317f6358961fd96197d2a3edb3ffbb00deff1cfa05fa1494c849393784bb89ae3f41425982f609f73bad0855a5e6522c328d + checksum: 10c0/95e79e50fa1a28cbfaf982def6ff82c5ecf18fe7eca077b4bc4cba1d43580541fcb1c38effbd299acff97f77372d8bc910642adc3d6e610c5125a9f0b85f379e languageName: node linkType: hard @@ -8548,14 +8548,14 @@ __metadata: languageName: node linkType: hard -"react-activity-calendar@npm:^2.7.8": - version: 2.7.8 - resolution: "react-activity-calendar@npm:2.7.8" +"react-activity-calendar@npm:^2.7.11": + version: 2.7.12 + resolution: "react-activity-calendar@npm:2.7.12" dependencies: date-fns: "npm:^4.1.0" peerDependencies: react: ^18.0.0 || ^19.0.0 - checksum: 10c0/e18d17cadc65480a8fa1df390598d7a4741a211b164091ca85fd2a21673fa667a5921f7b856e4ed92c2e8dedfb961426f163e517ea37336f2c3afb987740620d + checksum: 10c0/6e329c18d37e05c9f26efee3af229e9289744d9f6bac03a08b4911b62786a410d0f7cfe58ace1e52706da4adc250e82906a77ef6d2665e52d1e271edcdd8b60e languageName: node linkType: hard @@ -8630,14 +8630,14 @@ __metadata: languageName: node linkType: hard -"react-error-boundary@npm:^5.0.0": - version: 5.0.0 - resolution: "react-error-boundary@npm:5.0.0" +"react-error-boundary@npm:^6.0.0": + version: 6.0.0 + resolution: "react-error-boundary@npm:6.0.0" dependencies: "@babel/runtime": "npm:^7.12.5" peerDependencies: react: ">=16.13.1" - checksum: 10c0/38da5e7e81016a4750d3b090e3c740c2c1125c0bb9de14e1ab92ee3b5190d34517c199935302718a24aa35d3f89081412b3444edc23f63729bde2e862a2fbfec + checksum: 10c0/1914d600dee95a14f14af4afe9867b0d35c26c4f7826d23208800ba2a99728659029aad60a6ef95e13430b4d79c2c4c9b3585f50bf508450478760d2e4e732d8 languageName: node linkType: hard @@ -8665,14 +8665,14 @@ __metadata: linkType: hard "react-github-calendar@npm:^4.5.1": - version: 4.5.6 - resolution: "react-github-calendar@npm:4.5.6" + version: 4.5.7 + resolution: "react-github-calendar@npm:4.5.7" dependencies: - react-activity-calendar: "npm:^2.7.8" - react-error-boundary: "npm:^5.0.0" + react-activity-calendar: "npm:^2.7.11" + react-error-boundary: "npm:^6.0.0" peerDependencies: react: ^18.0.0 || ^19.0.0 - checksum: 10c0/74e995a528a3cf2a4ac4634415b614a0f6ad170f066d2d58142f15b815e8855de8b2ecf426ea054dd5d29143185dedc9a4ec7a8fa4dea4cbad6b7ef0f4411b5a + checksum: 10c0/a274776d8f6547d342573a42618196f436d4e555a5eb84b7b43fda0350a5174959ae74bc9d230c8cdca1b8982e4f586583200292126d4aa05cb1e3d350b579d4 languageName: node linkType: hard @@ -9241,12 +9241,12 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.7.1": - version: 7.7.1 - resolution: "semver@npm:7.7.1" +"semver@npm:7.7.2": + version: 7.7.2 + resolution: "semver@npm:7.7.2" bin: semver: bin/semver.js - checksum: 10c0/fd603a6fb9c399c6054015433051bdbe7b99a940a8fb44b85c2b524c4004b023d7928d47cb22154f8d054ea7ee8597f586605e05b52047f048278e4ac56ae958 + checksum: 10c0/aca305edfbf2383c22571cb7714f48cadc7ac95371b4b52362fb8eeffdfbc0de0669368b82b2b15978f8848f01d7114da65697e56cd8c37b0dab8c58e543f9ea languageName: node linkType: hard @@ -10221,13 +10221,6 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.20.0": - version: 6.20.0 - resolution: "undici-types@npm:6.20.0" - checksum: 10c0/68e659a98898d6a836a9a59e6adf14a5d799707f5ea629433e025ac90d239f75e408e2e5ff086afc3cace26f8b26ee52155293564593fbb4a2f666af57fc59bf - languageName: node - linkType: hard - "undici-types@npm:~6.21.0": version: 6.21.0 resolution: "undici-types@npm:6.21.0" @@ -10235,6 +10228,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~7.8.0": + version: 7.8.0 + resolution: "undici-types@npm:7.8.0" + checksum: 10c0/9d9d246d1dc32f318d46116efe3cfca5a72d4f16828febc1918d94e58f6ffcf39c158aa28bf5b4fc52f410446bc7858f35151367bd7a49f21746cab6497b709b + languageName: node + linkType: hard + "unified@npm:^10.0.0": version: 10.1.2 resolution: "unified@npm:10.1.2" @@ -10369,10 +10369,10 @@ __metadata: "@types/react-router-dom": "npm:5.3.3" "@types/react-table": "npm:7.7.20" "@types/react-test-renderer": "npm:18.3.1" - "@types/semver": "npm:7.5.8" + "@types/semver": "npm:7.7.0" "@types/uuid": "npm:^9.0.0" - "@uiw/codemirror-theme-duotone": "npm:4.23.10" - "@uiw/react-codemirror": "npm:4.23.10" + "@uiw/codemirror-theme-duotone": "npm:4.23.13" + "@uiw/react-codemirror": "npm:4.23.13" "@unleash/proxy-client-react": "npm:^5.0.0" "@vitejs/plugin-react": "npm:4.3.4" cartesian: "npm:^1.0.1" @@ -10422,7 +10422,7 @@ __metadata: react-table: "npm:7.8.0" react-test-renderer: "npm:18.3.1" sass: "npm:1.85.1" - semver: "npm:7.7.1" + semver: "npm:7.7.2" swr: "npm:2.3.3" tss-react: "npm:4.9.15" typescript: "npm:5.8.3" diff --git a/package.json b/package.json index 1a1534ee99..c9798fc86a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "unleash-server", "type": "module", "description": "Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.", - "version": "7.0.1", + "version": "7.0.3", "keywords": [ "unleash", "feature flag", @@ -129,7 +129,7 @@ "ts-toolbelt": "^9.6.0", "type-is": "^1.6.18", "ulidx": "^2.4.1", - "unleash-client": "^6.6.0", + "unleash-client": "^6.7.0-beta.0", "uuid": "^9.0.0" }, "devDependencies": { @@ -141,25 +141,25 @@ "@swc/core": "1.11.31", "@types/bcryptjs": "2.4.6", "@types/cors": "2.8.19", - "@types/express": "4.17.21", - "@types/express-session": "1.18.1", + "@types/express": "4.17.23", + "@types/express-session": "1.18.2", "@types/faker": "5.5.9", "@types/hash-sum": "^1.0.0", "@types/js-yaml": "4.0.9", "@types/lodash.groupby": "4.6.9", "@types/lodash.isequal": "^4.5.8", - "@types/memoizee": "0.4.11", + "@types/memoizee": "0.4.12", "@types/mime": "4.0.0", "@types/murmurhash3js": "^3.0.7", "@types/mustache": "^4.2.5", "@types/node": "22.15.18", "@types/nodemailer": "6.4.17", "@types/owasp-password-strength-test": "1.3.2", - "@types/pg": "8.15.2", - "@types/semver": "7.5.8", + "@types/pg": "8.15.4", + "@types/semver": "7.7.0", "@types/slug": "^5.0.8", "@types/stoppable": "1.1.3", - "@types/supertest": "6.0.2", + "@types/supertest": "6.0.3", "@types/type-is": "1.6.7", "@types/uuid": "9.0.8", "@vitest/coverage-v8": "^3.1.3", @@ -167,7 +167,7 @@ "concurrently": "^9.0.0", "copyfiles": "2.4.1", "coveralls": "^3.1.1", - "del-cli": "5.1.0", + "del-cli": "6.0.0", "faker": "5.5.3", "fast-check": "3.23.2", "fetch-mock": "^12.0.0", diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml index 049f7d4cd1..54421c87a3 100644 --- a/scripts/docker-compose.yml +++ b/scripts/docker-compose.yml @@ -11,7 +11,7 @@ services: - 5432:5432 pgadmin: - image: dpage/pgadmin4:9.3 + image: dpage/pgadmin4:9.4 environment: PGADMIN_DEFAULT_EMAIL: 'admin@admin.com' PGADMIN_DEFAULT_PASSWORD: 'admin' diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 22116ade5f..455feba0d7 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -101,6 +101,7 @@ exports[`should create default config 1`] = ` "preHook": undefined, "preRouterHook": undefined, "prometheusApi": undefined, + "prometheusImpactMetricsApi": undefined, "publicFolder": undefined, "rateLimiting": { "callSignalEndpointMaxPerSecond": 1, diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index 45b9b588cd..b631cf7456 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -773,6 +773,10 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { defaultDaysToBeConsideredInactive, ); + const prometheusImpactMetricsApi = + options.prometheusImpactMetricsApi || + process.env.PROMETHEUS_IMPACT_METRICS_API; + return { db, session, @@ -804,6 +808,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { clientFeatureCaching, accessControlMaxAge, prometheusApi, + prometheusImpactMetricsApi, publicFolder: options.publicFolder, disableScheduler: options.disableScheduler, isEnterprise: isEnterprise, diff --git a/src/lib/db/client-instance-store.ts b/src/lib/db/client-instance-store.ts index bf61a076cb..516259153e 100644 --- a/src/lib/db/client-instance-store.ts +++ b/src/lib/db/client-instance-store.ts @@ -71,35 +71,6 @@ export default class ClientInstanceStore implements IClientInstanceStore { } } - /** - * @deprecated - * `bulkUpsert` is beeing used instead. remove with `lastSeenBulkQuery` flag - */ - async setLastSeen({ - appName, - instanceId, - environment, - clientIp, - }: INewClientInstance): Promise { - const stopTimer = this.metricTimer('setLastSeen'); - - await this.db(TABLE) - .insert({ - app_name: appName, - instance_id: instanceId, - environment, - last_seen: new Date(), - client_ip: clientIp, - }) - .onConflict(['app_name', 'instance_id', 'environment']) - .merge({ - last_seen: new Date(), - client_ip: clientIp, - }); - - stopTimer(); - } - async bulkUpsert(instances: INewClientInstance[]): Promise { const stopTimer = this.metricTimer('bulkUpsert'); diff --git a/src/lib/features/events/event-service.ts b/src/lib/features/events/event-service.ts index 8ec37385e0..b55ddf0bd5 100644 --- a/src/lib/features/events/event-service.ts +++ b/src/lib/features/events/event-service.ts @@ -222,7 +222,7 @@ export default class EventService { if (parsed) queryParams.push(parsed); } - ['project', 'type', 'environment'].forEach((field) => { + ['project', 'type', 'environment', 'id'].forEach((field) => { if (params[field]) { const parsed = parseSearchOperatorValue(field, params[field]); if (parsed) queryParams.push(parsed); diff --git a/src/lib/features/feature-toggle/feature-toggle-controller.ts b/src/lib/features/feature-toggle/feature-toggle-controller.ts index 1b2bdcd6cf..3380a61a09 100644 --- a/src/lib/features/feature-toggle/feature-toggle-controller.ts +++ b/src/lib/features/feature-toggle/feature-toggle-controller.ts @@ -918,11 +918,6 @@ export default class ProjectFeaturesController extends Controller { const { shouldActivateDisabledStrategies } = req.query; const { features } = req.body; - if (this.flagResolver.isEnabled('disableBulkToggle')) { - res.status(403).end(); - return; - } - await this.transactionalFeatureToggleService.transactional((service) => service.bulkUpdateEnabled( projectId, @@ -950,11 +945,6 @@ export default class ProjectFeaturesController extends Controller { const { shouldActivateDisabledStrategies } = req.query; const { features } = req.body; - if (this.flagResolver.isEnabled('disableBulkToggle')) { - res.status(403).end(); - return; - } - await this.transactionalFeatureToggleService.transactional((service) => service.bulkUpdateEnabled( projectId, diff --git a/src/lib/features/metrics/client-metrics/metrics-service-v2.ts b/src/lib/features/metrics/client-metrics/metrics-service-v2.ts index 00271320a9..d2e2da83f2 100644 --- a/src/lib/features/metrics/client-metrics/metrics-service-v2.ts +++ b/src/lib/features/metrics/client-metrics/metrics-service-v2.ts @@ -6,7 +6,7 @@ import type { IClientMetricsEnv, IClientMetricsStoreV2, } from './client-metrics-store-v2-type.js'; -import { clientMetricsSchema } from '../shared/schema.js'; +import { clientMetricsSchema, impactMetricsSchema } from '../shared/schema.js'; import { compareAsc, secondsToMilliseconds } from 'date-fns'; import { CLIENT_METRICS, @@ -30,6 +30,11 @@ import { MAX_UNKNOWN_FLAGS, type UnknownFlagsService, } from '../unknown-flags/unknown-flags-service.js'; +import { + type Metric, + MetricsTranslator, +} from '../impact/metrics-translator.js'; +import { impactRegister } from '../impact/impact-register.js'; export default class ClientMetricsServiceV2 { private config: IUnleashConfig; @@ -46,6 +51,8 @@ export default class ClientMetricsServiceV2 { private logger: Logger; + private impactMetricsTranslator: MetricsTranslator; + private cachedFeatureNames: () => Promise; constructor( @@ -69,6 +76,7 @@ export default class ClientMetricsServiceV2 { maxAge: secondsToMilliseconds(10), }, ); + this.impactMetricsTranslator = new MetricsTranslator(impactRegister); } async clearMetrics(hoursAgo: number) { @@ -187,6 +195,11 @@ export default class ClientMetricsServiceV2 { this.lastSeenService.updateLastSeen(metrics); } + async registerImpactMetrics(impactMetrics: Metric[]) { + const value = await impactMetricsSchema.validateAsync(impactMetrics); + this.impactMetricsTranslator.translateMetrics(value); + } + async registerClientMetrics( data: ClientMetricsSchema, clientIp: string, diff --git a/src/lib/features/metrics/impact/impact-metrics.e2e.test.ts b/src/lib/features/metrics/impact/impact-metrics.e2e.test.ts new file mode 100644 index 0000000000..a1eb68ffb3 --- /dev/null +++ b/src/lib/features/metrics/impact/impact-metrics.e2e.test.ts @@ -0,0 +1,103 @@ +import { + type IUnleashTest, + setupAppWithCustomConfig, +} from '../../../../test/e2e/helpers/test-helper.js'; +import dbInit, { + type ITestDb, +} from '../../../../test/e2e/helpers/database-init.js'; +import getLogger from '../../../../test/fixtures/no-logger.js'; +import type { Metric } from './metrics-translator.js'; + +let app: IUnleashTest; +let db: ITestDb; + +const sendImpactMetrics = async (impactMetrics: Metric[], status = 202) => + app.request + .post('/api/client/metrics') + .send({ + appName: 'impact-metrics-app', + instanceId: 'instance-id', + bucket: { + start: Date.now(), + stop: Date.now(), + toggles: {}, + }, + impactMetrics, + }) + .expect(status); + +beforeAll(async () => { + db = await dbInit('impact_metrics', getLogger); + app = await setupAppWithCustomConfig(db.stores, { + experimental: { + flags: { + impactMetrics: true, + }, + }, + }); +}); + +afterAll(async () => { + await app.destroy(); + await db.destroy(); +}); + +test('should store impact metrics in memory and be able to retrieve them', async () => { + await sendImpactMetrics([ + { + name: 'labeled_counter', + help: 'with labels', + type: 'counter', + samples: [ + { + labels: { foo: 'bar' }, + value: 5, + }, + ], + }, + ]); + + await sendImpactMetrics([ + { + name: 'labeled_counter', + help: 'with labels', + type: 'counter', + samples: [ + { + labels: { foo: 'bar' }, + value: 10, + }, + ], + }, + ]); + + await sendImpactMetrics([]); + // missing help + await sendImpactMetrics( + [ + // @ts-expect-error + { + name: 'labeled_counter', + type: 'counter', + samples: [ + { + labels: { foo: 'bar' }, + value: 10, + }, + ], + }, + ], + 400, + ); + + const response = await app.request + .get('/internal-backstage/impact/metrics') + .expect('Content-Type', /text/) + .expect(200); + + const metricsText = response.text; + + expect(metricsText).toContain('# HELP labeled_counter with labels'); + expect(metricsText).toContain('# TYPE labeled_counter counter'); + expect(metricsText).toMatch(/labeled_counter{foo="bar"} 15/); +}); diff --git a/src/lib/features/metrics/impact/metrics-translator.ts b/src/lib/features/metrics/impact/metrics-translator.ts index 0f43128606..f27999d837 100644 --- a/src/lib/features/metrics/impact/metrics-translator.ts +++ b/src/lib/features/metrics/impact/metrics-translator.ts @@ -1,11 +1,11 @@ import { Counter, Gauge, type Registry } from 'prom-client'; -interface MetricSample { +export interface MetricSample { labels?: Record; value: number; } -interface Metric { +export interface Metric { name: string; help: string; type: 'counter' | 'gauge'; diff --git a/src/lib/features/metrics/instance/instance-service.ts b/src/lib/features/metrics/instance/instance-service.ts index 82a8aea1a4..acc4ef4c53 100644 --- a/src/lib/features/metrics/instance/instance-service.ts +++ b/src/lib/features/metrics/instance/instance-service.ts @@ -101,21 +101,12 @@ export default class ClientInstanceService { ): Promise { const value = await clientMetricsSchema.validateAsync(data); - if (this.flagResolver.isEnabled('lastSeenBulkQuery')) { - this.seenClients[this.clientKey(value)] = { - appName: value.appName, - instanceId: value.instanceId, - environment: value.environment, - clientIp: clientIp, - }; - } else { - await this.clientInstanceStore.setLastSeen({ - appName: value.appName, - instanceId: value.instanceId, - environment: value.environment, - clientIp: clientIp, - }); - } + this.seenClients[this.clientKey(value)] = { + appName: value.appName, + instanceId: value.instanceId, + environment: value.environment, + clientIp: clientIp, + }; } public registerFrontendClient(data: IFrontendClientApp): void { diff --git a/src/lib/features/metrics/instance/metrics.ts b/src/lib/features/metrics/instance/metrics.ts index 649bdd12e8..8eabc96a82 100644 --- a/src/lib/features/metrics/instance/metrics.ts +++ b/src/lib/features/metrics/instance/metrics.ts @@ -27,6 +27,7 @@ import { CLIENT_METRICS } from '../../../events/index.js'; import type { CustomMetricsSchema } from '../../../openapi/spec/custom-metrics-schema.js'; import type { StoredCustomMetric } from '../custom/custom-metrics-store.js'; import type { CustomMetricsService } from '../custom/custom-metrics-service.js'; +import type { MetricsTranslator } from '../impact/metrics-translator.js'; export default class ClientMetricsController extends Controller { logger: Logger; @@ -39,6 +40,8 @@ export default class ClientMetricsController extends Controller { customMetricsService: CustomMetricsService; + metricsTranslator: MetricsTranslator; + flagResolver: IFlagResolver; constructor( @@ -150,16 +153,25 @@ export default class ClientMetricsController extends Controller { } else { try { const { body: data, ip: clientIp, user } = req; - data.environment = this.metricsV2.resolveMetricsEnvironment( - user, - data, - ); + const { impactMetrics, ...metricsData } = data; + metricsData.environment = + this.metricsV2.resolveMetricsEnvironment(user, metricsData); await this.clientInstanceService.registerInstance( - data, + metricsData, clientIp, ); - await this.metricsV2.registerClientMetrics(data, clientIp); + await this.metricsV2.registerClientMetrics( + metricsData, + clientIp, + ); + if ( + this.flagResolver.isEnabled('impactMetrics') && + impactMetrics + ) { + await this.metricsV2.registerImpactMetrics(impactMetrics); + } + res.getHeaderNames().forEach((header) => res.removeHeader(header), ); diff --git a/src/lib/features/metrics/shared/schema.ts b/src/lib/features/metrics/shared/schema.ts index 683e00a8b9..fa5d1ed586 100644 --- a/src/lib/features/metrics/shared/schema.ts +++ b/src/lib/features/metrics/shared/schema.ts @@ -85,6 +85,35 @@ export const customMetricsSchema = joi metrics: joi.array().items(customMetricSchema).required(), }); +export const metricSampleSchema = joi + .object() + .options({ stripUnknown: true }) + .keys({ + value: joi.number().required(), + labels: joi + .object() + .pattern( + joi.string(), + joi.alternatives().try(joi.string(), joi.number()), + ) + .optional(), + }); + +export const impactMetricSchema = joi + .object() + .options({ stripUnknown: true }) + .keys({ + name: joi.string().required(), + help: joi.string().required(), + type: joi.string().required(), + samples: joi.array().items(metricSampleSchema).required(), + }); + +export const impactMetricsSchema = joi + .array() + .items(impactMetricSchema) + .empty(); + export const batchMetricsSchema = joi .object() .options({ stripUnknown: true }) diff --git a/src/lib/openapi/spec/event-search-query-parameters.ts b/src/lib/openapi/spec/event-search-query-parameters.ts index d778aa738c..a36fc971c8 100644 --- a/src/lib/openapi/spec/event-search-query-parameters.ts +++ b/src/lib/openapi/spec/event-search-query-parameters.ts @@ -11,6 +11,17 @@ export const eventSearchQueryParameters = [ 'Find events by a free-text search query. The query will be matched against the event data payload (if any).', in: 'query', }, + { + name: 'id', + schema: { + type: 'string', + example: 'IS:123', + pattern: '^(IS|IS_ANY_OF):(.*?)(,([0-9]+))*$', + }, + description: + 'Filter by event ID using supported operators: IS, IS_ANY_OF.', + in: 'query', + }, { name: 'feature', schema: { diff --git a/src/lib/server-impl.ts b/src/lib/server-impl.ts index 54ac5d01b1..21e2cef94a 100644 --- a/src/lib/server-impl.ts +++ b/src/lib/server-impl.ts @@ -1,7 +1,7 @@ import stoppable, { type StoppableServer } from 'stoppable'; import { promisify } from 'util'; import version from './util/version.js'; -import { migrateDb, resetDb } from '../migrator.js'; +import { migrateDb, requiresMigration, resetDb } from '../migrator.js'; import getApp from './app.js'; import type MetricsMonitor from './metrics.js'; import { createMetricsMonitor } from './metrics.js'; @@ -336,21 +336,25 @@ async function start( if (config.db.disableMigration) { logger.info('DB migration: disabled'); } else { - logger.info('DB migration: start'); - if (config.flagResolver.isEnabled('migrationLock')) { - logger.info('Running migration with lock'); - const lock = withDbLock(config.db, { - lockKey: defaultLockKey, - timeout: defaultTimeout, - logger, - }); - await lock(migrateDb)(config); - } else { - logger.info('Running migration without lock'); - await migrateDb(config); - } + if (await requiresMigration(config)) { + logger.info('DB migration: start'); + if (config.flagResolver.isEnabled('migrationLock')) { + logger.info('Running migration with lock'); + const lock = withDbLock(config.db, { + lockKey: defaultLockKey, + timeout: defaultTimeout, + logger, + }); + await lock(migrateDb)(config); + } else { + logger.info('Running migration without lock'); + await migrateDb(config); + } - logger.info('DB migration: end'); + logger.info('DB migration: end'); + } else { + logger.info('DB migration: no migration needed'); + } } } catch (err) { logger.error('Failed to migrate db', err); diff --git a/src/lib/services/email-service.ts b/src/lib/services/email-service.ts index 64fe92ef85..9c2f2b0259 100644 --- a/src/lib/services/email-service.ts +++ b/src/lib/services/email-service.ts @@ -52,7 +52,8 @@ const SCHEDULED_CHANGE_CONFLICT_SUBJECT = 'Unleash - Scheduled changes can no longer be applied'; const SCHEDULED_EXECUTION_FAILED_SUBJECT = 'Unleash - Scheduled change request could not be applied'; - +const REQUESTED_CR_APPROVAL_SUBJECT = + 'Unleash - new change request waiting to be reviewed'; export const MAIL_ACCEPTED = '250 Accepted'; export type ChangeRequestScheduleConflictData = @@ -121,6 +122,67 @@ export class EmailService { } } + async sendRequestedCRApprovalEmail( + recipient: string, + changeRequestLink: string, + changeRequestTitle: string, + ): Promise { + if (this.configured()) { + const year = new Date().getFullYear(); + const bodyHtml = await this.compileTemplate( + 'requested-cr-approval', + TemplateFormat.HTML, + { + changeRequestLink, + changeRequestTitle, + year, + }, + ); + const bodyText = await this.compileTemplate( + 'requested-cr-approval', + TemplateFormat.PLAIN, + { + changeRequestLink, + changeRequestTitle, + year, + }, + ); + const email = { + from: this.sender, + to: recipient, + subject: REQUESTED_CR_APPROVAL_SUBJECT, + html: bodyHtml, + text: bodyText, + }; + process.nextTick(() => { + this.mailer!.sendMail(email).then( + () => + this.logger.info( + 'Successfully sent requested-cr-approval email', + ), + (e) => + this.logger.warn( + 'Failed to send requested-cr-approval email', + e, + ), + ); + }); + return Promise.resolve(email); + } + return new Promise((res) => { + this.logger.warn( + 'No mailer is configured. Please read the docs on how to configure an email service', + ); + this.logger.debug('Change request link: ', changeRequestLink); + res({ + from: this.sender, + to: recipient, + subject: REQUESTED_CR_APPROVAL_SUBJECT, + html: '', + text: '', + }); + }); + } async sendScheduledExecutionFailedEmail( recipient: string, changeRequestLink: string, diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index cc3a22d432..aefdcb3d72 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -17,7 +17,6 @@ export type IFlagKey = | 'migrationLock' | 'demo' | 'googleAuthEnabled' - | 'disableBulkToggle' | 'advancedPlayground' | 'filterInvalidClientMetrics' | 'disableMetrics' @@ -57,7 +56,6 @@ export type IFlagKey = | 'edgeObservability' | 'registerFrontendClient' | 'reportUnknownFlags' - | 'lastSeenBulkQuery' | 'lifecycleMetrics' | 'customMetrics' | 'impactMetrics' @@ -105,10 +103,6 @@ const flags: IFlags = { process.env.GOOGLE_AUTH_ENABLED, false, ), - disableBulkToggle: parseEnvVarBoolean( - process.env.DISABLE_BULK_TOGGLE, - false, - ), filterInvalidClientMetrics: parseEnvVarBoolean( process.env.FILTER_INVALID_CLIENT_METRICS, false, @@ -272,10 +266,6 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_REPORT_UNKNOWN_FLAGS, false, ), - lastSeenBulkQuery: parseEnvVarBoolean( - process.env.UNLEASH_EXPERIMENTAL_LAST_SEEN_BULK_QUERY, - false, - ), lifecycleMetrics: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_LIFECYCLE_METRICS, false, @@ -319,10 +309,19 @@ export interface IFlagResolver { isEnabled: (expName: IFlagKey, context?: IFlagContext) => boolean; getVariant: (expName: IFlagKey, context?: IFlagContext) => Variant; getStaticContext: () => IFlagContext; + impactMetrics?: IImpactMetricsResolver; } export interface IExternalFlagResolver { isEnabled: (flagName: IFlagKey, context?: IFlagContext) => boolean; getVariant: (flagName: IFlagKey, context?: IFlagContext) => Variant; getStaticContext: () => IFlagContext; + impactMetrics?: IImpactMetricsResolver; +} + +export interface IImpactMetricsResolver { + defineCounter(name: string, help: string); + defineGauge(name: string, help: string); + incrementCounter(name: string, value?: number, featureName?: string): void; + updateGauge(name: string, value: number, featureName?: string): void; } diff --git a/src/lib/types/option.ts b/src/lib/types/option.ts index fedb1eca03..6af0a001f9 100644 --- a/src/lib/types/option.ts +++ b/src/lib/types/option.ts @@ -164,6 +164,7 @@ export interface IUnleashOptions { clientFeatureCaching?: Partial; accessControlMaxAge?: number; prometheusApi?: string; + prometheusImpactMetricsApi?: string; publicFolder?: string; disableScheduler?: boolean; metricsRateLimiting?: Partial; @@ -288,6 +289,7 @@ export interface IUnleashConfig { clientFeatureCaching: IClientCachingOption; accessControlMaxAge: number; prometheusApi?: string; + prometheusImpactMetricsApi?: string; publicFolder?: string; disableScheduler?: boolean; isEnterprise: boolean; diff --git a/src/lib/types/stores/client-instance-store.ts b/src/lib/types/stores/client-instance-store.ts index e835664cce..36695cd474 100644 --- a/src/lib/types/stores/client-instance-store.ts +++ b/src/lib/types/stores/client-instance-store.ts @@ -18,11 +18,6 @@ export interface IClientInstanceStore Pick > { bulkUpsert(instances: INewClientInstance[]): Promise; - /** - * @deprecated - * `bulkUpsert` is beeing used instead. remove with `lastSeenBulkQuery` flag - */ - setLastSeen(INewClientInstance): Promise; insert(details: INewClientInstance): Promise; getByAppName(appName: string): Promise; getRecentByAppNameAndEnvironment( diff --git a/src/lib/types/stores/event-store.ts b/src/lib/types/stores/event-store.ts index 0ee004b0e8..581cd03316 100644 --- a/src/lib/types/stores/event-store.ts +++ b/src/lib/types/stores/event-store.ts @@ -6,6 +6,7 @@ import type { IQueryOperations } from '../../features/events/event-store.js'; import type { IQueryParam } from '../../features/feature-toggle/types/feature-toggle-strategies-store-type.js'; export interface IEventSearchParams { + id?: string; project?: string; query?: string; feature?: string; diff --git a/src/mailtemplates/requested-cr-approval/requested-cr-approval.html.mustache b/src/mailtemplates/requested-cr-approval/requested-cr-approval.html.mustache new file mode 100644 index 0000000000..222f25a351 --- /dev/null +++ b/src/mailtemplates/requested-cr-approval/requested-cr-approval.html.mustache @@ -0,0 +1,379 @@ + + + + + *|MC:SUBJECT|* + + + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + +
+ +
+ +
+ + + + + +
+

You have been added to review {{{ changeRequestTitle }}}

+

Click {{{changeRequestLink}}} to review it

+
+ +
+ + + + + + + + +
+ Unleash blog + Github + Slack community + Help center +
+ Copyright © {{ year }} | Bricks Software | All rights reserved. +
+ +
+ +
+
+ + diff --git a/src/mailtemplates/requested-cr-approval/requested-cr-approval.plain.mustache b/src/mailtemplates/requested-cr-approval/requested-cr-approval.plain.mustache new file mode 100644 index 0000000000..6e74deb6ad --- /dev/null +++ b/src/mailtemplates/requested-cr-approval/requested-cr-approval.plain.mustache @@ -0,0 +1,3 @@ +You have been added to review {{{ changeRequestTitle }}} + +Follow the link: {{{ changeRequestLink }}} to review it. diff --git a/src/migrations/20250618090103-create-cr-requested-approvals.js b/src/migrations/20250618090103-create-cr-requested-approvals.js new file mode 100644 index 0000000000..409efac666 --- /dev/null +++ b/src/migrations/20250618090103-create-cr-requested-approvals.js @@ -0,0 +1,19 @@ +exports.up = function(db, cb) { + db.runSql(`CREATE TABLE change_request_requested_approvers( + change_request_id INTEGER REFERENCES change_requests(id) ON DELETE CASCADE, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + requested_at TIMESTAMP WITH TIME ZONE DEFAULT (now() at time zone 'utc'), + PRIMARY KEY (change_request_id, user_id) + ); + CREATE INDEX IF NOT EXISTS change_request_requested_approvers_cr_id_idx ON change_request_requested_approvers(change_request_id); + CREATE INDEX IF NOT EXISTS change_request_requested_approvers_user_id_idx ON change_request_requested_approvers(user_id); + `, cb) +}; + +exports.down = function(db, cb) { + db.runSql(`DROP TABLE IF EXISTS change_request_requested_approvers`, cb); +}; + +exports._meta = { + "version": 1 +}; diff --git a/src/migrations/20250623100820-cr-requested-approvals-add-notified-at-column.js b/src/migrations/20250623100820-cr-requested-approvals-add-notified-at-column.js new file mode 100644 index 0000000000..46d44f701c --- /dev/null +++ b/src/migrations/20250623100820-cr-requested-approvals-add-notified-at-column.js @@ -0,0 +1,10 @@ +exports.up = function(db, cb) { + db.runSql(`ALTER TABLE change_request_requested_approvers ADD COLUMN notified_at TIMESTAMP WITH TIME ZONE; + CREATE INDEX IF NOT EXISTS cr_req_approvers_notified_at_idx ON change_request_requested_approvers(notified_at);`, cb); +}; + +exports.down = function(db, cb) { + db.runSql(` +DROP INDEX IF EXISTS cr_req_approvers_notified_at_idx; +ALTER TABLE change_request_requested_approvers DROP COLUMN notified_at;`, cb); +}; diff --git a/src/migrator.ts b/src/migrator.ts index 82027b290b..8543cefb98 100644 --- a/src/migrator.ts +++ b/src/migrator.ts @@ -40,6 +40,28 @@ export async function migrateDb( }); } +export async function requiresMigration({ + db, +}: Pick): Promise { + return noDatabaseUrl(async () => { + const custom = { + ...db, + connectionTimeoutMillis: secondsToMilliseconds(10), + }; + + // disable Intellij/WebStorm from setting verbose CLI argument to db-migrator + process.argv = process.argv.filter((it) => !it.includes('--verbose')); + const dbm = getInstance(true, { + cwd: __dirname, + config: { custom }, + env: 'custom', + }); + + const pendingMigrations = await dbm.check(); + return pendingMigrations.length > 0; + }); +} + // This exists to ease testing export async function resetDb({ db }: IUnleashConfig): Promise { return noDatabaseUrl(async () => { diff --git a/src/server-dev.ts b/src/server-dev.ts index 2d12dba8e5..b49244861d 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -56,6 +56,7 @@ process.nextTick(async () => { customMetrics: true, lifecycleMetrics: true, improvedJsonDiff: true, + impactMetrics: true, }, }, authentication: { @@ -69,6 +70,7 @@ process.nextTick(async () => { }, ], }, + prometheusImpactMetricsApi: 'http://localhost:9090', /* can be tweaked to control configuration caching for /api/client/features clientFeatureCaching: { enabled: true, diff --git a/src/test/e2e/api/admin/event-search.e2e.test.ts b/src/test/e2e/api/admin/event-search.e2e.test.ts index 8cfae7f7b8..778ab0c9c1 100644 --- a/src/test/e2e/api/admin/event-search.e2e.test.ts +++ b/src/test/e2e/api/admin/event-search.e2e.test.ts @@ -618,3 +618,103 @@ test('should filter events by environment using IS_ANY_OF', async () => { total: 2, }); }); + +test('should filter events by ID', async () => { + await eventService.storeEvent({ + type: FEATURE_CREATED, + project: 'default', + data: { name: 'feature1' }, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }); + + await eventService.storeEvent({ + type: FEATURE_CREATED, + project: 'default', + data: { name: 'feature2' }, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }); + + await eventService.storeEvent({ + type: FEATURE_CREATED, + project: 'default', + data: { name: 'feature3' }, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }); + + const { body: allEventsResponse } = await searchEvents({}); + const targetEvent = allEventsResponse.events.find( + (e: any) => e.data.name === 'feature2', + ); + + const { body } = await searchEvents({ id: `IS:${targetEvent.id}` }); + + expect(body).toMatchObject({ + events: [ + { + id: targetEvent.id, + type: 'feature-created', + data: { name: 'feature2' }, + }, + ], + total: 1, + }); +}); + +test('should filter events by multiple IDs using IS_ANY_OF', async () => { + await eventService.storeEvent({ + type: FEATURE_CREATED, + project: 'default', + data: { name: 'feature1' }, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }); + + await eventService.storeEvent({ + type: FEATURE_CREATED, + project: 'default', + data: { name: 'feature2' }, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }); + + await eventService.storeEvent({ + type: FEATURE_CREATED, + project: 'default', + data: { name: 'feature3' }, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }); + + const { body: allEventsResponse } = await searchEvents({}); + const targetEvent1 = allEventsResponse.events.find( + (e: any) => e.data.name === 'feature1', + ); + const targetEvent3 = allEventsResponse.events.find( + (e: any) => e.data.name === 'feature3', + ); + + const { body } = await searchEvents({ + id: `IS_ANY_OF:${targetEvent1.id},${targetEvent3.id}`, + }); + + expect(body.total).toBe(2); + expect(body.events).toHaveLength(2); + + const returnedIds = body.events.map((e: any) => e.id); + expect(returnedIds).toContain(targetEvent1.id); + expect(returnedIds).toContain(targetEvent3.id); + + const feature2Event = allEventsResponse.events.find( + (e: any) => e.data.name === 'feature2', + ); + expect(returnedIds).not.toContain(feature2Event.id); +}); diff --git a/src/test/e2e/stores/event-store.e2e.test.ts b/src/test/e2e/stores/event-store.e2e.test.ts index 1b1b507dbd..81a0def9a2 100644 --- a/src/test/e2e/stores/event-store.e2e.test.ts +++ b/src/test/e2e/stores/event-store.e2e.test.ts @@ -338,3 +338,137 @@ test('getMaxRevisionId should exclude FEATURE_CREATED and FEATURE_TAGGED events' expect(updatedEvent!.id).toBeGreaterThan(taggedEvent!.id); expect(segmentEvent!.id).toBeGreaterThan(updatedEvent!.id); }); + +test('Should filter events by ID using IS operator', async () => { + const event1 = { + type: FEATURE_CREATED, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + data: { name: 'feature1' }, + }; + const event2 = { + type: FEATURE_CREATED, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + data: { name: 'feature2' }, + }; + const event3 = { + type: FEATURE_CREATED, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + data: { name: 'feature3' }, + }; + + await eventStore.store(event1); + await eventStore.store(event2); + await eventStore.store(event3); + + const allEvents = await eventStore.getAll(); + const targetEvent = allEvents.find((e) => e.data.name === 'feature2'); + + const filteredEvents = await eventStore.searchEvents( + { + offset: 0, + limit: 10, + }, + [ + { + field: 'id', + operator: 'IS', + values: [targetEvent!.id.toString()], + }, + ], + ); + + expect(filteredEvents).toHaveLength(1); + expect(filteredEvents[0].id).toBe(targetEvent!.id); + expect(filteredEvents[0].data.name).toBe('feature2'); +}); + +test('Should filter events by ID using IS_ANY_OF operator', async () => { + const event1 = { + type: FEATURE_CREATED, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + data: { name: 'feature1' }, + }; + const event2 = { + type: FEATURE_CREATED, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + data: { name: 'feature2' }, + }; + const event3 = { + type: FEATURE_CREATED, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + data: { name: 'feature3' }, + }; + + await eventStore.store(event1); + await eventStore.store(event2); + await eventStore.store(event3); + + const allEvents = await eventStore.getAll(); + const targetEvent1 = allEvents.find((e) => e.data.name === 'feature1'); + const targetEvent3 = allEvents.find((e) => e.data.name === 'feature3'); + + const filteredEvents = await eventStore.searchEvents( + { + offset: 0, + limit: 10, + }, + [ + { + field: 'id', + operator: 'IS_ANY_OF', + values: [ + targetEvent1!.id.toString(), + targetEvent3!.id.toString(), + ], + }, + ], + ); + + expect(filteredEvents).toHaveLength(2); + const eventIds = filteredEvents.map((e) => e.id); + expect(eventIds).toContain(targetEvent1!.id); + expect(eventIds).toContain(targetEvent3!.id); + expect(eventIds).not.toContain( + allEvents.find((e) => e.data.name === 'feature2')!.id, + ); +}); + +test('Should return empty result when filtering by non-existent ID', async () => { + const event = { + type: FEATURE_CREATED, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + data: { name: 'feature1' }, + }; + + await eventStore.store(event); + + const filteredEvents = await eventStore.searchEvents( + { + offset: 0, + limit: 10, + }, + [ + { + field: 'id', + operator: 'IS', + values: ['999999'], + }, + ], + ); + + expect(filteredEvents).toHaveLength(0); +}); diff --git a/src/test/fixtures/fake-client-instance-store.ts b/src/test/fixtures/fake-client-instance-store.ts index ffcce9dea4..4be697f1a2 100644 --- a/src/test/fixtures/fake-client-instance-store.ts +++ b/src/test/fixtures/fake-client-instance-store.ts @@ -28,10 +28,6 @@ export default class FakeClientInstanceStore implements IClientInstanceStore { ); } - setLastSeen(): Promise { - return Promise.resolve(); - } - async getBySdkName(sdkName: string): Promise { return this.instances.filter((instance) => instance.sdkVersion?.startsWith(sdkName), diff --git a/yarn.lock b/yarn.lock index 3370fae807..8e8fef902f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -95,15 +95,6 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0": - version: 7.18.6 - resolution: "@babel/code-frame@npm:7.18.6" - dependencies: - "@babel/highlight": "npm:^7.18.6" - checksum: 10c0/e3966f2717b7ebd9610524730e10b75ee74154f62617e5e115c97dbbbabc5351845c9aa850788012cb4d9aee85c3dc59fe6bef36690f244e8dcfca34bd35e9c9 - languageName: node - linkType: hard - "@babel/code-frame@npm:^7.25.9": version: 7.26.0 resolution: "@babel/code-frame@npm:7.26.0" @@ -232,13 +223,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.18.6": - version: 7.19.1 - resolution: "@babel/helper-validator-identifier@npm:7.19.1" - checksum: 10c0/f978ecfea840f65b64ab9e17fac380625a45f4fe1361eeb29867fcfd1c9eaa72abd7023f2f40ac3168587d7e5153660d16cfccb352a557be2efd347a051b4b20 - languageName: node - linkType: hard - "@babel/helper-validator-identifier@npm:^7.25.9": version: 7.25.9 resolution: "@babel/helper-validator-identifier@npm:7.25.9" @@ -270,17 +254,6 @@ __metadata: languageName: node linkType: hard -"@babel/highlight@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/highlight@npm:7.18.6" - dependencies: - "@babel/helper-validator-identifier": "npm:^7.18.6" - chalk: "npm:^2.0.0" - js-tokens: "npm:^4.0.0" - checksum: 10c0/a6a6928d25099ef04c337fcbb829fab8059bb67d31ac37212efd611bdbe247d0e71a5096c4524272cb56399f40251fac57c025e42d3bc924db0183a6435a60ac - languageName: node - linkType: hard - "@babel/parser@npm:^7.25.4": version: 7.27.2 resolution: "@babel/parser@npm:7.27.2" @@ -1170,6 +1143,13 @@ __metadata: languageName: node linkType: hard +"@sindresorhus/merge-streams@npm:^2.1.0": + version: 2.3.0 + resolution: "@sindresorhus/merge-streams@npm:2.3.0" + checksum: 10c0/69ee906f3125fb2c6bb6ec5cdd84e8827d93b49b3892bce8b62267116cc7e197b5cccf20c160a1d32c26014ecd14470a72a5e3ee37a58f1d6dadc0db1ccf3894 + languageName: node + linkType: hard + "@slack/logger@npm:^4.0.0": version: 4.0.0 resolution: "@slack/logger@npm:4.0.0" @@ -1437,12 +1417,12 @@ __metadata: languageName: node linkType: hard -"@types/express-session@npm:1.18.1": - version: 1.18.1 - resolution: "@types/express-session@npm:1.18.1" +"@types/express-session@npm:1.18.2": + version: 1.18.2 + resolution: "@types/express-session@npm:1.18.2" dependencies: "@types/express": "npm:*" - checksum: 10c0/df2d439239f5cc9947772452d1d86d47a8d3e33f90776ebcf602f0fb7801bc85f6410d17557f40cdde881816bc5a2804a7373addfd3a5dbd8c54e7af8aa27000 + checksum: 10c0/5d5aa134ce8990920b35f2dd0aa55168af44faaf14789b6921d361ce016c43bdc66feba287753981a2fee33fd95b8a829c4418c3ca480b03961724b8bc13e453 languageName: node linkType: hard @@ -1458,15 +1438,15 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:4.17.21": - version: 4.17.21 - resolution: "@types/express@npm:4.17.21" +"@types/express@npm:4.17.23": + version: 4.17.23 + resolution: "@types/express@npm:4.17.23" dependencies: "@types/body-parser": "npm:*" "@types/express-serve-static-core": "npm:^4.17.33" "@types/qs": "npm:*" "@types/serve-static": "npm:*" - checksum: 10c0/12e562c4571da50c7d239e117e688dc434db1bac8be55613294762f84fd77fbd0658ccd553c7d3ab02408f385bc93980992369dd30e2ecd2c68c358e6af8fabf + checksum: 10c0/60490cd4f73085007247e7d4fafad0a7abdafa34fa3caba2757512564ca5e094ece7459f0f324030a63d513f967bb86579a8682af76ae2fd718e889b0a2a4fe8 languageName: node linkType: hard @@ -1537,10 +1517,10 @@ __metadata: languageName: node linkType: hard -"@types/memoizee@npm:0.4.11": - version: 0.4.11 - resolution: "@types/memoizee@npm:0.4.11" - checksum: 10c0/4a8114e26971186b29b81add600e34ad45578f8847b5cf04209509f9f21c334171205405f19658f6176419fdad09cf18c084c21802257d71517f91761b97a10c +"@types/memoizee@npm:0.4.12": + version: 0.4.12 + resolution: "@types/memoizee@npm:0.4.12" + checksum: 10c0/573ca0c7e3db4306679bd0fbee92063e03406cd14d2bc4409d9a61ba731f1bb3925d3183a28ed6a8f480b3326ca03bdef6766c1649aa29592200f97fd0d38955 languageName: node linkType: hard @@ -1567,13 +1547,6 @@ __metadata: languageName: node linkType: hard -"@types/minimist@npm:^1.2.2": - version: 1.2.2 - resolution: "@types/minimist@npm:1.2.2" - checksum: 10c0/f220f57f682bbc3793dab4518f8e2180faa79d8e2589c79614fd777d7182be203ba399020c3a056a115064f5d57a065004a32b522b2737246407621681b24137 - languageName: node - linkType: hard - "@types/murmurhash3js@npm:^3.0.7": version: 3.0.7 resolution: "@types/murmurhash3js@npm:3.0.7" @@ -1582,9 +1555,9 @@ __metadata: linkType: hard "@types/mustache@npm:^4.2.5": - version: 4.2.5 - resolution: "@types/mustache@npm:4.2.5" - checksum: 10c0/624975c39068d47407eadb89628aaff5ef60f3b7a71eef92a254310896a4e90518a01dcf71d95779ab2c986034a6ca5403d22fea237c67ff87f2e2b3fb794ea6 + version: 4.2.6 + resolution: "@types/mustache@npm:4.2.6" + checksum: 10c0/f49a83b189e92c962e9b61094c80c979f115ea876bd746bdb9f725c38ab8981a6691e3d1d5cd008ade24b6e5040602970bc0bc60ade71772501d1df8adc7adad languageName: node linkType: hard @@ -1624,13 +1597,6 @@ __metadata: languageName: node linkType: hard -"@types/normalize-package-data@npm:^2.4.0": - version: 2.4.1 - resolution: "@types/normalize-package-data@npm:2.4.1" - checksum: 10c0/c90b163741f27a1a4c3b1869d7d5c272adbd355eb50d5f060f9ce122ce4342cf35f5b0005f55ef780596cacfeb69b7eee54cd3c2e02d37f75e664945b6e75fc6 - languageName: node - linkType: hard - "@types/owasp-password-strength-test@npm:1.3.2": version: 1.3.2 resolution: "@types/owasp-password-strength-test@npm:1.3.2" @@ -1638,14 +1604,14 @@ __metadata: languageName: node linkType: hard -"@types/pg@npm:8.15.2": - version: 8.15.2 - resolution: "@types/pg@npm:8.15.2" +"@types/pg@npm:8.15.4": + version: 8.15.4 + resolution: "@types/pg@npm:8.15.4" dependencies: "@types/node": "npm:*" pg-protocol: "npm:*" - pg-types: "npm:^4.0.1" - checksum: 10c0/e3bc75f02af897ed960e83d1af9bd0cba1ff41cd0cbae0eaee323eae84f55e6d433f620aa1c72f7bd5107c80b018185c0e47de553cfc5439514c3da98768ef6c + pg-types: "npm:^2.2.0" + checksum: 10c0/7f9295cb2d934681bba84f7caad529c3b100d87e83ad0732c7fe496f4f79e42a795097321db54e010fcff22cb5e410cf683b4c9941907ee4564c822242816e91 languageName: node linkType: hard @@ -1670,10 +1636,10 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:7.5.8": - version: 7.5.8 - resolution: "@types/semver@npm:7.5.8" - checksum: 10c0/8663ff927234d1c5fcc04b33062cb2b9fcfbe0f5f351ed26c4d1e1581657deebd506b41ff7fdf89e787e3d33ce05854bc01686379b89e9c49b564c4cfa988efa +"@types/semver@npm:7.7.0": + version: 7.7.0 + resolution: "@types/semver@npm:7.7.0" + checksum: 10c0/6b5f65f647474338abbd6ee91a6bbab434662ddb8fe39464edcbcfc96484d388baad9eb506dff217b6fc1727a88894930eb1f308617161ac0f376fe06be4e1ee languageName: node linkType: hard @@ -1714,13 +1680,13 @@ __metadata: languageName: node linkType: hard -"@types/supertest@npm:6.0.2": - version: 6.0.2 - resolution: "@types/supertest@npm:6.0.2" +"@types/supertest@npm:6.0.3": + version: 6.0.3 + resolution: "@types/supertest@npm:6.0.3" dependencies: "@types/methods": "npm:^1.1.4" "@types/superagent": "npm:^8.1.0" - checksum: 10c0/44a28f9b35b65800f4c7bcc23748e71c925098aa74ea504d14c98385c36d00de2a4f5aca15d7dc4514bc80533e0af21f985a4ab9f5f317c7266e9e75836aef39 + checksum: 10c0/a2080f870154b09db123864a484fb633bc9e2a0f7294a194388df4c7effe5af9de36d5a5ebf819f72b404fa47b5e813c47d5a3a51354251fd2fa8589bfb64f2c languageName: node linkType: hard @@ -1935,16 +1901,6 @@ __metadata: languageName: node linkType: hard -"aggregate-error@npm:^4.0.0": - version: 4.0.1 - resolution: "aggregate-error@npm:4.0.1" - dependencies: - clean-stack: "npm:^4.0.0" - indent-string: "npm:^5.0.0" - checksum: 10c0/75fd739f5c4c60a667cce35ccaf0edf135e147ef0be9a029cab75de14ac9421779b15339d562e58d25b233ea0ef2bbd4c916f149fdbcb73c2b9a62209e611343 - languageName: node - linkType: hard - "ajv-draft-04@npm:^1.0.0": version: 1.0.0 resolution: "ajv-draft-04@npm:1.0.0" @@ -2036,15 +1992,6 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^3.2.1": - version: 3.2.1 - resolution: "ansi-styles@npm:3.2.1" - dependencies: - color-convert: "npm:^1.9.0" - checksum: 10c0/ece5a8ef069fcc5298f67e3f4771a663129abd174ea2dfa87923a2be2abf6cd367ef72ac87942da00ce85bd1d651d4cd8595aebdb1b385889b89b205860e977b - languageName: node - linkType: hard - "ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": version: 4.3.0 resolution: "ansi-styles@npm:4.3.0" @@ -2105,13 +2052,6 @@ __metadata: languageName: node linkType: hard -"arrify@npm:^1.0.1": - version: 1.0.1 - resolution: "arrify@npm:1.0.1" - checksum: 10c0/c35c8d1a81bcd5474c0c57fe3f4bad1a4d46a5fa353cedcff7a54da315df60db71829e69104b859dff96c5d68af46bd2be259fe5e50dc6aa9df3b36bea0383ab - languageName: node - linkType: hard - "asap@npm:^2.0.0": version: 2.0.6 resolution: "asap@npm:2.0.6" @@ -2349,18 +2289,6 @@ __metadata: languageName: node linkType: hard -"camelcase-keys@npm:^7.0.0": - version: 7.0.2 - resolution: "camelcase-keys@npm:7.0.2" - dependencies: - camelcase: "npm:^6.3.0" - map-obj: "npm:^4.1.0" - quick-lru: "npm:^5.1.1" - type-fest: "npm:^1.2.1" - checksum: 10c0/ae86a51168643e9e8a2f2c7bfa17850729979ec3dafc5253056a7d97931cbb0e3ef5b4185e59d54b7a56c54405dee2874b0c82033498d8626e512ff9034cb05c - languageName: node - linkType: hard - "camelcase@npm:^5.0.0": version: 5.3.1 resolution: "camelcase@npm:5.3.1" @@ -2368,13 +2296,6 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^6.3.0": - version: 6.3.0 - resolution: "camelcase@npm:6.3.0" - checksum: 10c0/0d701658219bd3116d12da3eab31acddb3f9440790c0792e0d398f0a520a6a4058018e546862b6fba89d7ae990efaeb97da71e1913e9ebf5a8b5621a3d55c710 - languageName: node - linkType: hard - "caniuse-lite@npm:^1.0.30001663": version: 1.0.30001669 resolution: "caniuse-lite@npm:1.0.30001669" @@ -2402,17 +2323,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^2.0.0": - version: 2.4.2 - resolution: "chalk@npm:2.4.2" - dependencies: - ansi-styles: "npm:^3.2.1" - escape-string-regexp: "npm:^1.0.5" - supports-color: "npm:^5.3.0" - checksum: 10c0/e6543f02ec877732e3a2d1c3c3323ddb4d39fbab687c23f526e25bd4c6a9bf3b83a696e8c769d078e04e5754921648f7821b2a2acfd16c550435fd630026e073 - languageName: node - linkType: hard - "chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -2451,15 +2361,6 @@ __metadata: languageName: node linkType: hard -"clean-stack@npm:^4.0.0": - version: 4.2.0 - resolution: "clean-stack@npm:4.2.0" - dependencies: - escape-string-regexp: "npm:5.0.0" - checksum: 10c0/2bdf981a0fef0a23c14255df693b30eb9ae27eedf212470d8c400a0c0b6fb82fbf1ff8c5216ccd5721e3670b700389c886b1dce5070776dc9fbcc040957758c0 - languageName: node - linkType: hard - "cli-cursor@npm:^5.0.0": version: 5.0.0 resolution: "cli-cursor@npm:5.0.0" @@ -2525,15 +2426,6 @@ __metadata: languageName: node linkType: hard -"color-convert@npm:^1.9.0": - version: 1.9.3 - resolution: "color-convert@npm:1.9.3" - dependencies: - color-name: "npm:1.1.3" - checksum: 10c0/5ad3c534949a8c68fca8fbc6f09068f435f0ad290ab8b2f76841b9e6af7e0bb57b98cb05b0e19fe33f5d91e5a8611ad457e5f69e0a484caad1f7487fd0e8253c - languageName: node - linkType: hard - "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -2543,13 +2435,6 @@ __metadata: languageName: node linkType: hard -"color-name@npm:1.1.3": - version: 1.1.3 - resolution: "color-name@npm:1.1.3" - checksum: 10c0/566a3d42cca25b9b3cd5528cd7754b8e89c0eb646b7f214e8e2eaddb69994ac5f0557d9c175eb5d8f0ad73531140d9c47525085ee752a91a2ab15ab459caf6d6 - languageName: node - linkType: hard - "color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" @@ -2972,30 +2857,13 @@ __metadata: languageName: node linkType: hard -"decamelize-keys@npm:^1.1.0": - version: 1.1.1 - resolution: "decamelize-keys@npm:1.1.1" - dependencies: - decamelize: "npm:^1.1.0" - map-obj: "npm:^1.0.0" - checksum: 10c0/4ca385933127437658338c65fb9aead5f21b28d3dd3ccd7956eb29aab0953b5d3c047fbc207111672220c71ecf7a4d34f36c92851b7bbde6fca1a02c541bdd7d - languageName: node - linkType: hard - -"decamelize@npm:^1.1.0, decamelize@npm:^1.2.0": +"decamelize@npm:^1.2.0": version: 1.2.0 resolution: "decamelize@npm:1.2.0" checksum: 10c0/85c39fe8fbf0482d4a1e224ef0119db5c1897f8503bcef8b826adff7a1b11414972f6fef2d7dec2ee0b4be3863cf64ac1439137ae9e6af23a3d8dcbe26a5b4b2 languageName: node linkType: hard -"decamelize@npm:^5.0.0": - version: 5.0.1 - resolution: "decamelize@npm:5.0.1" - checksum: 10c0/3da71022bc1e85487810fa0833138effb599fa331ca21e179650e93a765d0c4dabeb1ecdd6ad1474fa0bacd2457953c63ea335afb6e53b35f2b4bf779514e2a3 - languageName: node - linkType: hard - "deep-eql@npm:^5.0.1": version: 5.0.2 resolution: "deep-eql@npm:5.0.2" @@ -3035,32 +2903,30 @@ __metadata: languageName: node linkType: hard -"del-cli@npm:5.1.0": - version: 5.1.0 - resolution: "del-cli@npm:5.1.0" +"del-cli@npm:6.0.0": + version: 6.0.0 + resolution: "del-cli@npm:6.0.0" dependencies: - del: "npm:^7.1.0" - meow: "npm:^10.1.3" + del: "npm:^8.0.0" + meow: "npm:^13.2.0" bin: del: cli.js del-cli: cli.js - checksum: 10c0/555dfbbdf254ec758f86eaa3960779ae378c11fb9f734ba9f09b1019237919a66ffdaa286dad4b8df3cb8f315157fd41b6ff63afb7fa46a6c61eed7bbac04488 + checksum: 10c0/920a57efd804afab7799b8304de97d3ebbaf98dc0a524a4938115a494d67bf116674e3b38375c9cd091cf7caa8b4c2a32cbda3a032f66e0554d30d03ed5eddbe languageName: node linkType: hard -"del@npm:^7.1.0": - version: 7.1.0 - resolution: "del@npm:7.1.0" +"del@npm:^8.0.0": + version: 8.0.0 + resolution: "del@npm:8.0.0" dependencies: - globby: "npm:^13.1.2" - graceful-fs: "npm:^4.2.10" + globby: "npm:^14.0.2" is-glob: "npm:^4.0.3" is-path-cwd: "npm:^3.0.0" is-path-inside: "npm:^4.0.0" - p-map: "npm:^5.5.0" - rimraf: "npm:^3.0.2" - slash: "npm:^4.0.0" - checksum: 10c0/5ad2777b69e386b414ba77f5eba23bb52422c096f4c084c0d1d829ee4776d1a025a6f69765906907c4137026e9bd071ee9d422fd531b1417ef546adc7eb6fada + p-map: "npm:^7.0.2" + slash: "npm:^5.1.0" + checksum: 10c0/dd9099dc245173caad16a6372c7c9eb316e19e75e7bebfdce86ee59572c2591be5e569e15e8768108bb451f5319c57407cfa7adf74424f150d4d29c7f6da5601 languageName: node linkType: hard @@ -3109,15 +2975,6 @@ __metadata: languageName: node linkType: hard -"dir-glob@npm:^3.0.1": - version: 3.0.1 - resolution: "dir-glob@npm:3.0.1" - dependencies: - path-type: "npm:^4.0.0" - checksum: 10c0/dcac00920a4d503e38bb64001acb19df4efc14536ada475725e12f52c16777afdee4db827f55f13a908ee7efc0cb282e2e3dbaeeb98c0993dd93d1802d3bf00c - languageName: node - linkType: hard - "dotenv@npm:^5.0.1": version: 5.0.1 resolution: "dotenv@npm:5.0.1" @@ -3244,15 +3101,6 @@ __metadata: languageName: node linkType: hard -"error-ex@npm:^1.3.1": - version: 1.3.2 - resolution: "error-ex@npm:1.3.2" - dependencies: - is-arrayish: "npm:^0.2.1" - checksum: 10c0/ba827f89369b4c93382cfca5a264d059dfefdaa56ecc5e338ffa58a6471f5ed93b71a20add1d52290a4873d92381174382658c885ac1a2305f7baca363ce9cce - languageName: node - linkType: hard - "errorhandler@npm:^1.5.1": version: 1.5.1 resolution: "errorhandler@npm:1.5.1" @@ -3438,20 +3286,6 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:5.0.0": - version: 5.0.0 - resolution: "escape-string-regexp@npm:5.0.0" - checksum: 10c0/6366f474c6f37a802800a435232395e04e9885919873e382b157ab7e8f0feb8fed71497f84a6f6a81a49aab41815522f5839112bd38026d203aea0c91622df95 - languageName: node - linkType: hard - -"escape-string-regexp@npm:^1.0.5": - version: 1.0.5 - resolution: "escape-string-regexp@npm:1.0.5" - checksum: 10c0/a968ad453dd0c2724e14a4f20e177aaf32bb384ab41b674a8454afe9a41c5e6fe8903323e0a1052f56289d04bd600f81278edf140b0fcc02f5cac98d0f5b5371 - languageName: node - linkType: hard - "esm@npm:^3.2.25": version: 3.2.25 resolution: "esm@npm:3.2.25" @@ -3693,16 +3527,16 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.2.11": - version: 3.2.12 - resolution: "fast-glob@npm:3.2.12" +"fast-glob@npm:^3.3.3": + version: 3.3.3 + resolution: "fast-glob@npm:3.3.3" dependencies: "@nodelib/fs.stat": "npm:^2.0.2" "@nodelib/fs.walk": "npm:^1.2.3" glob-parent: "npm:^5.1.2" merge2: "npm:^1.3.0" - micromatch: "npm:^4.0.4" - checksum: 10c0/08604fb8ef6442ce74068bef3c3104382bb1f5ab28cf75e4ee904662778b60ad620e1405e692b7edea598ef445f5d387827a965ba034e1892bf54b1dfde97f26 + micromatch: "npm:^4.0.8" + checksum: 10c0/f6aaa141d0d3384cf73cbcdfc52f475ed293f6d5b65bfc5def368b09163a9f7e5ec2b3014d80f733c405f58e470ee0cc451c2937685045cddcdeaa24199c43fe languageName: node linkType: hard @@ -3828,16 +3662,6 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^5.0.0": - version: 5.0.0 - resolution: "find-up@npm:5.0.0" - dependencies: - locate-path: "npm:^6.0.0" - path-exists: "npm:^4.0.0" - checksum: 10c0/062c5a83a9c02f53cdd6d175a37ecf8f87ea5bbff1fdfb828f04bfa021441bc7583e8ebc0872a4c1baab96221fb8a8a275a19809fb93fbc40bd69ec35634069a - languageName: node - linkType: hard - "flatted@npm:^3.2.7": version: 3.2.7 resolution: "flatted@npm:3.2.7" @@ -4136,7 +3960,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.0.5, glob@npm:^7.1.3": +"glob@npm:^7.0.5": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -4157,16 +3981,17 @@ __metadata: languageName: node linkType: hard -"globby@npm:^13.1.2": - version: 13.1.3 - resolution: "globby@npm:13.1.3" +"globby@npm:^14.0.2": + version: 14.1.0 + resolution: "globby@npm:14.1.0" dependencies: - dir-glob: "npm:^3.0.1" - fast-glob: "npm:^3.2.11" - ignore: "npm:^5.2.0" - merge2: "npm:^1.4.1" - slash: "npm:^4.0.0" - checksum: 10c0/34199932fad67ae6a4cca764eaad8e7678efabd4321f553bfb8a52046e03f8e8e2f9c14216a6734b692b7c26c4da1b1cfe9ce23733d28d1777d73f4bf34b09c7 + "@sindresorhus/merge-streams": "npm:^2.1.0" + fast-glob: "npm:^3.3.3" + ignore: "npm:^7.0.3" + path-type: "npm:^6.0.0" + slash: "npm:^5.1.0" + unicorn-magic: "npm:^0.3.0" + checksum: 10c0/527a1063c5958255969620c6fa4444a2b2e9278caddd571d46dfbfa307cb15977afb746e84d682ba5b6c94fc081e8997f80ff05dd235441ba1cb16f86153e58e languageName: node linkType: hard @@ -4186,13 +4011,6 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.2.10": - version: 4.2.10 - resolution: "graceful-fs@npm:4.2.10" - checksum: 10c0/4223a833e38e1d0d2aea630c2433cfb94ddc07dfc11d511dbd6be1d16688c5be848acc31f9a5d0d0ddbfb56d2ee5a6ae0278aceeb0ca6a13f27e06b9956fb952 - languageName: node - linkType: hard - "har-schema@npm:^2.0.0": version: 2.0.0 resolution: "har-schema@npm:2.0.0" @@ -4210,20 +4028,6 @@ __metadata: languageName: node linkType: hard -"hard-rejection@npm:^2.1.0": - version: 2.1.0 - resolution: "hard-rejection@npm:2.1.0" - checksum: 10c0/febc3343a1ad575aedcc112580835b44a89a89e01f400b4eda6e8110869edfdab0b00cd1bd4c3bfec9475a57e79e0b355aecd5be46454b6a62b9a359af60e564 - languageName: node - linkType: hard - -"has-flag@npm:^3.0.0": - version: 3.0.0 - resolution: "has-flag@npm:3.0.0" - checksum: 10c0/1c6c83b14b8b1b3c25b0727b8ba3e3b647f99e9e6e13eb7322107261de07a4c1be56fc0d45678fc376e09772a3a1642ccdaf8fc69bdf123b6c086598397ce473 - languageName: node - linkType: hard - "has-flag@npm:^4.0.0": version: 4.0.0 resolution: "has-flag@npm:4.0.0" @@ -4291,15 +4095,6 @@ __metadata: languageName: node linkType: hard -"hosted-git-info@npm:^4.0.1": - version: 4.1.0 - resolution: "hosted-git-info@npm:4.1.0" - dependencies: - lru-cache: "npm:^6.0.0" - checksum: 10c0/150fbcb001600336d17fdbae803264abed013548eea7946c2264c49ebe2ebd8c4441ba71dd23dd8e18c65de79d637f98b22d4760ba5fb2e0b15d62543d0fff07 - languageName: node - linkType: hard - "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -4411,10 +4206,10 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.2.0": - version: 5.2.4 - resolution: "ignore@npm:5.2.4" - checksum: 10c0/7c7cd90edd9fea6e037f9b9da4b01bf0a86b198ce78345f9bbd983929d68ff14830be31111edc5d70c264921f4962404d75b7262b4d9cc3bc12381eccbd03096 +"ignore@npm:^7.0.3": + version: 7.0.5 + resolution: "ignore@npm:7.0.5" + checksum: 10c0/ae00db89fe873064a093b8999fe4cc284b13ef2a178636211842cceb650b9c3e390d3339191acb145d81ed5379d2074840cf0c33a20bdbd6f32821f79eb4ad5d languageName: node linkType: hard @@ -4432,13 +4227,6 @@ __metadata: languageName: node linkType: hard -"indent-string@npm:^5.0.0": - version: 5.0.0 - resolution: "indent-string@npm:5.0.0" - checksum: 10c0/8ee77b57d92e71745e133f6f444d6fa3ed503ad0e1bcd7e80c8da08b42375c07117128d670589725ed07b1978065803fa86318c309ba45415b7fe13e7f170220 - languageName: node - linkType: hard - "inflection@npm:^1.10.0": version: 1.13.4 resolution: "inflection@npm:1.13.4" @@ -4508,13 +4296,6 @@ __metadata: languageName: node linkType: hard -"is-arrayish@npm:^0.2.1": - version: 0.2.1 - resolution: "is-arrayish@npm:0.2.1" - checksum: 10c0/e7fb686a739068bb70f860b39b67afc62acc62e36bb61c5f965768abce1873b379c563e61dd2adad96ebb7edf6651111b385e490cf508378959b0ed4cac4e729 - languageName: node - linkType: hard - "is-buffer@npm:^1.0.2, is-buffer@npm:^1.1.5": version: 1.1.6 resolution: "is-buffer@npm:1.1.6" @@ -4531,15 +4312,6 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.5.0": - version: 2.11.0 - resolution: "is-core-module@npm:2.11.0" - dependencies: - has: "npm:^1.0.3" - checksum: 10c0/fd8f78ef4e243c295deafa809f89381d89aff5aaf38bb63266b17ee6e34b6a051baa5bdc2365456863336d56af6a59a4c1df1256b4eff7d6b4afac618586b004 - languageName: node - linkType: hard - "is-core-module@npm:^2.9.0": version: 2.12.0 resolution: "is-core-module@npm:2.12.0" @@ -4644,13 +4416,6 @@ __metadata: languageName: node linkType: hard -"is-plain-obj@npm:^1.1.0": - version: 1.1.0 - resolution: "is-plain-obj@npm:1.1.0" - checksum: 10c0/daaee1805add26f781b413fdf192fc91d52409583be30ace35c82607d440da63cc4cac0ac55136716688d6c0a2c6ef3edb2254fecbd1fe06056d6bd15975ee8c - languageName: node - linkType: hard - "is-plain-object@npm:^2.0.1": version: 2.0.4 resolution: "is-plain-object@npm:2.0.4" @@ -4899,13 +4664,6 @@ __metadata: languageName: node linkType: hard -"json-parse-even-better-errors@npm:^2.3.0": - version: 2.3.1 - resolution: "json-parse-even-better-errors@npm:2.3.1" - checksum: 10c0/140932564c8f0b88455432e0f33c4cb4086b8868e37524e07e723f4eaedb9425bdc2bafd71bd1d9765bd15fd1e2d126972bc83990f55c467168c228c24d665f3 - languageName: node - linkType: hard - "json-schema-to-ts@npm:2.12.0": version: 2.12.0 resolution: "json-schema-to-ts@npm:2.12.0" @@ -5025,13 +4783,6 @@ __metadata: languageName: node linkType: hard -"kind-of@npm:^6.0.3": - version: 6.0.3 - resolution: "kind-of@npm:6.0.3" - checksum: 10c0/61cdff9623dabf3568b6445e93e31376bee1cdb93f8ba7033d86022c2a9b1791a1d9510e026e6465ebd701a6dd2f7b0808483ad8838341ac52f003f512e0b4c4 - languageName: node - linkType: hard - "knex@npm:3, knex@npm:^3.1.0": version: 3.1.0 resolution: "knex@npm:3.1.0" @@ -5122,13 +4873,6 @@ __metadata: languageName: node linkType: hard -"lines-and-columns@npm:^1.1.6": - version: 1.2.4 - resolution: "lines-and-columns@npm:1.2.4" - checksum: 10c0/3da6ee62d4cd9f03f5dc90b4df2540fb85b352081bee77fe4bbcd12c9000ead7f35e0a38b8d09a9bb99b13223446dd8689ff3c4959807620726d788701a83d2d - languageName: node - linkType: hard - "lint-staged@npm:15.4.3": version: 15.4.3 resolution: "lint-staged@npm:15.4.3" @@ -5172,15 +4916,6 @@ __metadata: languageName: node linkType: hard -"locate-path@npm:^6.0.0": - version: 6.0.0 - resolution: "locate-path@npm:6.0.0" - dependencies: - p-locate: "npm:^5.0.0" - checksum: 10c0/d3972ab70dfe58ce620e64265f90162d247e87159b6126b01314dd67be43d50e96a50b517bce2d9452a79409c7614054c277b5232377de50416564a77ac7aad3 - languageName: node - linkType: hard - "lodash.defaults@npm:^4.1.0": version: 4.2.0 resolution: "lodash.defaults@npm:4.2.0" @@ -5279,15 +5014,6 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^6.0.0": - version: 6.0.0 - resolution: "lru-cache@npm:6.0.0" - dependencies: - yallist: "npm:^4.0.0" - checksum: 10c0/cb53e582785c48187d7a188d3379c181b5ca2a9c78d2bce3e7dee36f32761d1c42983da3fe12b55cb74e1779fa94cdc2e5367c028a9b35317184ede0c07a30a9 - languageName: node - linkType: hard - "lru-cache@npm:^9.1.1": version: 9.1.1 resolution: "lru-cache@npm:9.1.1" @@ -5369,20 +5095,6 @@ __metadata: languageName: node linkType: hard -"map-obj@npm:^1.0.0": - version: 1.0.1 - resolution: "map-obj@npm:1.0.1" - checksum: 10c0/ccca88395e7d38671ed9f5652ecf471ecd546924be2fb900836b9da35e068a96687d96a5f93dcdfa94d9a27d649d2f10a84595590f89a347fb4dda47629dcc52 - languageName: node - linkType: hard - -"map-obj@npm:^4.1.0": - version: 4.3.0 - resolution: "map-obj@npm:4.3.0" - checksum: 10c0/1c19e1c88513c8abdab25c316367154c6a0a6a0f77e3e8c391bb7c0e093aefed293f539d026dc013d86219e5e4c25f23b0003ea588be2101ccd757bacc12d43b - languageName: node - linkType: hard - "map-stream@npm:~0.1.0": version: 0.1.0 resolution: "map-stream@npm:0.1.0" @@ -5413,23 +5125,10 @@ __metadata: languageName: node linkType: hard -"meow@npm:^10.1.3": - version: 10.1.5 - resolution: "meow@npm:10.1.5" - dependencies: - "@types/minimist": "npm:^1.2.2" - camelcase-keys: "npm:^7.0.0" - decamelize: "npm:^5.0.0" - decamelize-keys: "npm:^1.1.0" - hard-rejection: "npm:^2.1.0" - minimist-options: "npm:4.1.0" - normalize-package-data: "npm:^3.0.2" - read-pkg-up: "npm:^8.0.0" - redent: "npm:^4.0.0" - trim-newlines: "npm:^4.0.2" - type-fest: "npm:^1.2.2" - yargs-parser: "npm:^20.2.9" - checksum: 10c0/a513849022edd5ddcc41d28c679d31978abe414d9db5bc457e95e537a4327b2910fd2f699cdd883293f9a5da8951a50939bf60fbd62f7fe12b9ddf96a84b1b27 +"meow@npm:^13.2.0": + version: 13.2.0 + resolution: "meow@npm:13.2.0" + checksum: 10c0/d5b339ae314715bcd0b619dd2f8a266891928e21526b4800d49b4fba1cc3fff7e2c1ff5edd3344149fac841bc2306157f858e8c4d5eaee4d52ce52ad925664ce languageName: node linkType: hard @@ -5465,7 +5164,7 @@ __metadata: languageName: node linkType: hard -"merge2@npm:^1.3.0, merge2@npm:^1.4.1": +"merge2@npm:^1.3.0": version: 1.4.1 resolution: "merge2@npm:1.4.1" checksum: 10c0/254a8a4605b58f450308fc474c82ac9a094848081bf4c06778200207820e5193726dc563a0d2c16468810516a5c97d9d3ea0ca6585d23c58ccfff2403e8dbbeb @@ -5479,7 +5178,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.4, micromatch@npm:^4.0.8": +"micromatch@npm:^4.0.8": version: 4.0.8 resolution: "micromatch@npm:4.0.8" dependencies: @@ -5546,13 +5245,6 @@ __metadata: languageName: node linkType: hard -"min-indent@npm:^1.0.1": - version: 1.0.1 - resolution: "min-indent@npm:1.0.1" - checksum: 10c0/7e207bd5c20401b292de291f02913230cb1163abca162044f7db1d951fa245b174dc00869d40dd9a9f32a885ad6a5f3e767ee104cf278f399cb4e92d3f582d5c - languageName: node - linkType: hard - "minimatch@npm:^3.0.3, minimatch@npm:^3.1.1": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -5571,17 +5263,6 @@ __metadata: languageName: node linkType: hard -"minimist-options@npm:4.1.0": - version: 4.1.0 - resolution: "minimist-options@npm:4.1.0" - dependencies: - arrify: "npm:^1.0.1" - is-plain-obj: "npm:^1.1.0" - kind-of: "npm:^6.0.3" - checksum: 10c0/7871f9cdd15d1e7374e5b013e2ceda3d327a06a8c7b38ae16d9ef941e07d985e952c589e57213f7aa90a8744c60aed9524c0d85e501f5478382d9181f2763f54 - languageName: node - linkType: hard - "minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8": version: 1.2.8 resolution: "minimist@npm:1.2.8" @@ -5928,18 +5609,6 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^3.0.2": - version: 3.0.3 - resolution: "normalize-package-data@npm:3.0.3" - dependencies: - hosted-git-info: "npm:^4.0.1" - is-core-module: "npm:^2.5.0" - semver: "npm:^7.3.4" - validate-npm-package-license: "npm:^3.0.1" - checksum: 10c0/e5d0f739ba2c465d41f77c9d950e291ea4af78f8816ddb91c5da62257c40b76d8c83278b0d08ffbcd0f187636ebddad20e181e924873916d03e6e5ea2ef026be - languageName: node - linkType: hard - "normalize-url@npm:^6.1.0": version: 6.1.0 resolution: "normalize-url@npm:6.1.0" @@ -5977,13 +5646,6 @@ __metadata: languageName: node linkType: hard -"obuf@npm:~1.1.2": - version: 1.1.2 - resolution: "obuf@npm:1.1.2" - checksum: 10c0/520aaac7ea701618eacf000fc96ae458e20e13b0569845800fc582f81b386731ab22d55354b4915d58171db00e79cfcd09c1638c02f89577ef092b38c65b7d81 - languageName: node - linkType: hard - "on-finished@npm:2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -6074,15 +5736,6 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^3.0.2": - version: 3.1.0 - resolution: "p-limit@npm:3.1.0" - dependencies: - yocto-queue: "npm:^0.1.0" - checksum: 10c0/9db675949dbdc9c3763c89e748d0ef8bdad0afbb24d49ceaf4c46c02c77d30db4e0652ed36d0a0a7a95154335fab810d95c86153105bb73b3a90448e2bb14e1a - languageName: node - linkType: hard - "p-locate@npm:^4.1.0": version: 4.1.0 resolution: "p-locate@npm:4.1.0" @@ -6092,15 +5745,6 @@ __metadata: languageName: node linkType: hard -"p-locate@npm:^5.0.0": - version: 5.0.0 - resolution: "p-locate@npm:5.0.0" - dependencies: - p-limit: "npm:^3.0.2" - checksum: 10c0/2290d627ab7903b8b70d11d384fee714b797f6040d9278932754a6860845c4d3190603a0772a663c8cb5a7b21d1b16acb3a6487ebcafa9773094edc3dfe6009a - languageName: node - linkType: hard - "p-map@npm:^4.0.0": version: 4.0.0 resolution: "p-map@npm:4.0.0" @@ -6110,12 +5754,10 @@ __metadata: languageName: node linkType: hard -"p-map@npm:^5.5.0": - version: 5.5.0 - resolution: "p-map@npm:5.5.0" - dependencies: - aggregate-error: "npm:^4.0.0" - checksum: 10c0/410bce846b1e3db6bb2ccab6248372ecf4e635fc2b31331c8f56478e73fec9e146e8b4547585e635703160a3d252a6a65b8f855834aebc2c3408eb5789630cc4 +"p-map@npm:^7.0.2": + version: 7.0.3 + resolution: "p-map@npm:7.0.3" + checksum: 10c0/46091610da2b38ce47bcd1d8b4835a6fa4e832848a6682cf1652bc93915770f4617afc844c10a77d1b3e56d2472bb2d5622353fa3ead01a7f42b04fc8e744a5c languageName: node linkType: hard @@ -6171,18 +5813,6 @@ __metadata: languageName: node linkType: hard -"parse-json@npm:^5.2.0": - version: 5.2.0 - resolution: "parse-json@npm:5.2.0" - dependencies: - "@babel/code-frame": "npm:^7.0.0" - error-ex: "npm:^1.3.1" - json-parse-even-better-errors: "npm:^2.3.0" - lines-and-columns: "npm:^1.1.6" - checksum: 10c0/77947f2253005be7a12d858aedbafa09c9ae39eb4863adf330f7b416ca4f4a08132e453e08de2db46459256fb66afaac5ee758b44fe6541b7cdaf9d252e59585 - languageName: node - linkType: hard - "parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" @@ -6268,10 +5898,10 @@ __metadata: languageName: node linkType: hard -"path-type@npm:^4.0.0": - version: 4.0.0 - resolution: "path-type@npm:4.0.0" - checksum: 10c0/666f6973f332f27581371efaf303fd6c272cc43c2057b37aa99e3643158c7e4b2626549555d88626e99ea9e046f82f32e41bbde5f1508547e9a11b149b52387c +"path-type@npm:^6.0.0": + version: 6.0.0 + resolution: "path-type@npm:6.0.0" + checksum: 10c0/55baa8b1187d6dc683d5a9cfcc866168d6adff58e5db91126795376d818eee46391e00b2a4d53e44d844c7524a7d96aa68cc68f4f3e500d3d069a39e6535481c languageName: node linkType: hard @@ -6333,13 +5963,6 @@ __metadata: languageName: node linkType: hard -"pg-numeric@npm:1.0.2": - version: 1.0.2 - resolution: "pg-numeric@npm:1.0.2" - checksum: 10c0/43dd9884e7b52c79ddc28d2d282d7475fce8bba13452d33c04ceb2e0a65f561edf6699694e8e1c832ff9093770496363183c950dd29608e1bdd98f344b25bca9 - languageName: node - linkType: hard - "pg-pool@npm:^3.10.0": version: 3.10.0 resolution: "pg-pool@npm:3.10.0" @@ -6363,7 +5986,7 @@ __metadata: languageName: node linkType: hard -"pg-types@npm:2.2.0": +"pg-types@npm:2.2.0, pg-types@npm:^2.2.0": version: 2.2.0 resolution: "pg-types@npm:2.2.0" dependencies: @@ -6376,21 +5999,6 @@ __metadata: languageName: node linkType: hard -"pg-types@npm:^4.0.1": - version: 4.0.1 - resolution: "pg-types@npm:4.0.1" - dependencies: - pg-int8: "npm:1.0.1" - pg-numeric: "npm:1.0.2" - postgres-array: "npm:~3.0.1" - postgres-bytea: "npm:~3.0.0" - postgres-date: "npm:~2.0.1" - postgres-interval: "npm:^3.0.0" - postgres-range: "npm:^1.1.1" - checksum: 10c0/e2126b2775554ae8bacb3b104814487c2af2caff44cc52bee786b3887c65fe4c1fe031237e51e30ffed1cbb13b71776bd60cc1e65ac800c9946df4030849a074 - languageName: node - linkType: hard - "pg@npm:^8.11.2, pg@npm:^8.12.0": version: 8.16.0 resolution: "pg@npm:8.16.0" @@ -6491,13 +6099,6 @@ __metadata: languageName: node linkType: hard -"postgres-array@npm:~3.0.1": - version: 3.0.2 - resolution: "postgres-array@npm:3.0.2" - checksum: 10c0/644aa071f67a66a59f641f8e623887d2b915bc102a32643e2aa8b54c11acd343c5ad97831ea444dd37bd4b921ba35add4aa2cb0c6b76700a8252c2324aeba5b4 - languageName: node - linkType: hard - "postgres-bytea@npm:~1.0.0": version: 1.0.0 resolution: "postgres-bytea@npm:1.0.0" @@ -6505,15 +6106,6 @@ __metadata: languageName: node linkType: hard -"postgres-bytea@npm:~3.0.0": - version: 3.0.0 - resolution: "postgres-bytea@npm:3.0.0" - dependencies: - obuf: "npm:~1.1.2" - checksum: 10c0/41c79cc48aa730c5ba3eda6ab989a940034f07a1f57b8f2777dce56f1b8cca16c5870582932b5b10cc605048aef9b6157e06253c871b4717cafc6d00f55376aa - languageName: node - linkType: hard - "postgres-date@npm:~1.0.4": version: 1.0.7 resolution: "postgres-date@npm:1.0.7" @@ -6521,13 +6113,6 @@ __metadata: languageName: node linkType: hard -"postgres-date@npm:~2.0.1": - version: 2.0.1 - resolution: "postgres-date@npm:2.0.1" - checksum: 10c0/2d3698958f858b7d1df0a3929fb8750ccb43fa2c8ee9fec7a021e7926291f6c85ddd9d94d87cd6529d70bd2444f3e14fb5bb323af19ceaa733542cc05c5c653a - languageName: node - linkType: hard - "postgres-interval@npm:^1.1.0": version: 1.2.0 resolution: "postgres-interval@npm:1.2.0" @@ -6537,20 +6122,6 @@ __metadata: languageName: node linkType: hard -"postgres-interval@npm:^3.0.0": - version: 3.0.0 - resolution: "postgres-interval@npm:3.0.0" - checksum: 10c0/8b570b30ea37c685e26d136d34460f246f98935a1533defc4b53bb05ee23ae3dc7475b718ec7ea607a57894d8c6b4f1adf67ca9cc83a75bdacffd427d5c68de8 - languageName: node - linkType: hard - -"postgres-range@npm:^1.1.1": - version: 1.1.3 - resolution: "postgres-range@npm:1.1.3" - checksum: 10c0/f46bc379a198a9e3282a222c8e432d77494854bd4fa0706dff01641846db0bf4f09a9723e7fbb202da34ec3b2d88fc50e26e4bbeded7df19646e3acd6a7465ce - languageName: node - linkType: hard - "proc-log@npm:^3.0.0": version: 3.0.0 resolution: "proc-log@npm:3.0.0" @@ -6717,13 +6288,6 @@ __metadata: languageName: node linkType: hard -"quick-lru@npm:^5.1.1": - version: 5.1.1 - resolution: "quick-lru@npm:5.1.1" - checksum: 10c0/a24cba5da8cec30d70d2484be37622580f64765fb6390a928b17f60cd69e8dbd32a954b3ff9176fa1b86d86ff2ba05252fae55dc4d40d0291c60412b0ad096da - languageName: node - linkType: hard - "randexp@npm:^0.5.3": version: 0.5.3 resolution: "randexp@npm:0.5.3" @@ -6774,29 +6338,6 @@ __metadata: languageName: node linkType: hard -"read-pkg-up@npm:^8.0.0": - version: 8.0.0 - resolution: "read-pkg-up@npm:8.0.0" - dependencies: - find-up: "npm:^5.0.0" - read-pkg: "npm:^6.0.0" - type-fest: "npm:^1.0.1" - checksum: 10c0/cf3905ccbe5cd602f23192cc7ca65ed17561bab117eadb9aed817441d5bfc6b9a11215c2a3e9505f501d046818f3c4180dbea61fa83c42083e0b4e407d5cc745 - languageName: node - linkType: hard - -"read-pkg@npm:^6.0.0": - version: 6.0.0 - resolution: "read-pkg@npm:6.0.0" - dependencies: - "@types/normalize-package-data": "npm:^2.4.0" - normalize-package-data: "npm:^3.0.2" - parse-json: "npm:^5.2.0" - type-fest: "npm:^1.0.1" - checksum: 10c0/b51ee5eed75324f4fac34c9a40b5e4b403de4c532242be01959c9bbdb1ff9db1c6c2aefaba569622fec49d1ead866e97ba856ab145f6e11039b11f7bec1318ba - languageName: node - linkType: hard - "readable-stream@npm:~1.0.31": version: 1.0.34 resolution: "readable-stream@npm:1.0.34" @@ -6833,16 +6374,6 @@ __metadata: languageName: node linkType: hard -"redent@npm:^4.0.0": - version: 4.0.0 - resolution: "redent@npm:4.0.0" - dependencies: - indent-string: "npm:^5.0.0" - strip-indent: "npm:^4.0.0" - checksum: 10c0/a9b640c8f4b2b5b26a1a908706475ff404dd50a97d6f094bc3c59717be922622927cc7d601d4ae2857d897ad243fd979bd76d751a0481cee8be7024e5fb4c662 - languageName: node - linkType: hard - "regenerator-runtime@npm:^0.14.0": version: 0.14.0 resolution: "regenerator-runtime@npm:0.14.0" @@ -7024,17 +6555,6 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^3.0.2": - version: 3.0.2 - resolution: "rimraf@npm:3.0.2" - dependencies: - glob: "npm:^7.1.3" - bin: - rimraf: bin.js - checksum: 10c0/9cb7757acb489bd83757ba1a274ab545eafd75598a9d817e0c3f8b164238dd90eba50d6b848bd4dcc5f3040912e882dc7ba71653e35af660d77b25c381d402e8 - languageName: node - linkType: hard - "rimraf@npm:^5.0.5": version: 5.0.10 resolution: "rimraf@npm:5.0.10" @@ -7201,11 +6721,11 @@ __metadata: linkType: hard "semver@npm:^7.6.2": - version: 7.7.1 - resolution: "semver@npm:7.7.1" + version: 7.7.2 + resolution: "semver@npm:7.7.2" bin: semver: bin/semver.js - checksum: 10c0/fd603a6fb9c399c6054015433051bdbe7b99a940a8fb44b85c2b524c4004b023d7928d47cb22154f8d054ea7ee8597f586605e05b52047f048278e4ac56ae958 + checksum: 10c0/aca305edfbf2383c22571cb7714f48cadc7ac95371b4b52362fb8eeffdfbc0de0669368b82b2b15978f8848f01d7114da65697e56cd8c37b0dab8c58e543f9ea languageName: node linkType: hard @@ -7362,10 +6882,10 @@ __metadata: languageName: node linkType: hard -"slash@npm:^4.0.0": - version: 4.0.0 - resolution: "slash@npm:4.0.0" - checksum: 10c0/b522ca75d80d107fd30d29df0549a7b2537c83c4c4ecd12cd7d4ea6c8aaca2ab17ada002e7a1d78a9d736a0261509f26ea5b489082ee443a3a810586ef8eff18 +"slash@npm:^5.1.0": + version: 5.1.0 + resolution: "slash@npm:5.1.0" + checksum: 10c0/eb48b815caf0bdc390d0519d41b9e0556a14380f6799c72ba35caf03544d501d18befdeeef074bc9c052acf69654bc9e0d79d7f1de0866284137a40805299eb3 languageName: node linkType: hard @@ -7450,40 +6970,6 @@ __metadata: languageName: node linkType: hard -"spdx-correct@npm:^3.0.0": - version: 3.1.1 - resolution: "spdx-correct@npm:3.1.1" - dependencies: - spdx-expression-parse: "npm:^3.0.0" - spdx-license-ids: "npm:^3.0.0" - checksum: 10c0/25909eecc4024963a8e398399dbdd59ddb925bd7dbecd9c9cf6df0d75c29b68cd30b82123564acc51810eb02cfc4b634a2e16e88aa982433306012e318849249 - languageName: node - linkType: hard - -"spdx-exceptions@npm:^2.1.0": - version: 2.3.0 - resolution: "spdx-exceptions@npm:2.3.0" - checksum: 10c0/83089e77d2a91cb6805a5c910a2bedb9e50799da091f532c2ba4150efdef6e53f121523d3e2dc2573a340dc0189e648b03157097f65465b3a0c06da1f18d7e8a - languageName: node - linkType: hard - -"spdx-expression-parse@npm:^3.0.0": - version: 3.0.1 - resolution: "spdx-expression-parse@npm:3.0.1" - dependencies: - spdx-exceptions: "npm:^2.1.0" - spdx-license-ids: "npm:^3.0.0" - checksum: 10c0/6f8a41c87759fa184a58713b86c6a8b028250f158159f1d03ed9d1b6ee4d9eefdc74181c8ddc581a341aa971c3e7b79e30b59c23b05d2436d5de1c30bdef7171 - languageName: node - linkType: hard - -"spdx-license-ids@npm:^3.0.0": - version: 3.0.12 - resolution: "spdx-license-ids@npm:3.0.12" - checksum: 10c0/b749db2fdecf4ac1893b8e4c435c3bfe5247af9cb412a3cd8375c8bc5a24ad7f3c4263dfe0fc04701f98613f189787700f1deac3e9272c96dfaffc01826c2d0f - languageName: node - linkType: hard - "split2@npm:^4.1.0": version: 4.2.0 resolution: "split2@npm:4.2.0" @@ -7713,15 +7199,6 @@ __metadata: languageName: node linkType: hard -"strip-indent@npm:^4.0.0": - version: 4.0.0 - resolution: "strip-indent@npm:4.0.0" - dependencies: - min-indent: "npm:^1.0.1" - checksum: 10c0/6b1fb4e22056867f5c9e7a6f3f45922d9a2436cac758607d58aeaac0d3b16ec40b1c43317de7900f1b8dd7a4107352fa47fb960f2c23566538c51e8585c8870e - languageName: node - linkType: hard - "strip-json-comments@npm:~2.0.1": version: 2.0.1 resolution: "strip-json-comments@npm:2.0.1" @@ -7773,15 +7250,6 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^5.3.0": - version: 5.5.0 - resolution: "supports-color@npm:5.5.0" - dependencies: - has-flag: "npm:^3.0.0" - checksum: 10c0/6ae5ff319bfbb021f8a86da8ea1f8db52fac8bd4d499492e30ec17095b58af11f0c55f8577390a749b1c4dde691b6a0315dab78f5f54c9b3d83f8fb5905c1c05 - languageName: node - linkType: hard - "supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" @@ -8005,13 +7473,6 @@ __metadata: languageName: node linkType: hard -"trim-newlines@npm:^4.0.2": - version: 4.0.2 - resolution: "trim-newlines@npm:4.0.2" - checksum: 10c0/48d022e9d14f27cf8b71983691af61cd8ce511d159ed0962452d2fa23f58298398d905e1ff982566f9034f93df3ef676868c1c14d13bcd849e7500dbfbd6101b - languageName: node - linkType: hard - "truncate-utf8-bytes@npm:^1.0.0": version: 1.0.2 resolution: "truncate-utf8-bytes@npm:1.0.2" @@ -8130,13 +7591,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^1.0.1, type-fest@npm:^1.2.1, type-fest@npm:^1.2.2": - version: 1.4.0 - resolution: "type-fest@npm:1.4.0" - checksum: 10c0/a3c0f4ee28ff6ddf800d769eafafcdeab32efa38763c1a1b8daeae681920f6e345d7920bf277245235561d8117dab765cb5f829c76b713b4c9de0998a5397141 - languageName: node - linkType: hard - "type-is@npm:^1.6.18, type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" @@ -8213,6 +7667,13 @@ __metadata: languageName: node linkType: hard +"unicorn-magic@npm:^0.3.0": + version: 0.3.0 + resolution: "unicorn-magic@npm:0.3.0" + checksum: 10c0/0a32a997d6c15f1c2a077a15b1c4ca6f268d574cf5b8975e778bb98e6f8db4ef4e86dfcae4e158cd4c7e38fb4dd383b93b13eefddc7f178dea13d3ac8a603271 + languageName: node + linkType: hard + "unique-filename@npm:^3.0.0": version: 3.0.0 resolution: "unique-filename@npm:3.0.0" @@ -8245,9 +7706,9 @@ __metadata: languageName: node linkType: hard -"unleash-client@npm:^6.6.0": - version: 6.6.0 - resolution: "unleash-client@npm:6.6.0" +"unleash-client@npm:^6.7.0-beta.0": + version: 6.7.0-beta.0 + resolution: "unleash-client@npm:6.7.0-beta.0" dependencies: http-proxy-agent: "npm:^7.0.2" https-proxy-agent: "npm:^7.0.5" @@ -8257,7 +7718,7 @@ __metadata: murmurhash3js: "npm:^3.0.1" proxy-from-env: "npm:^1.1.0" semver: "npm:^7.6.2" - checksum: 10c0/d97805744874a0dd66af7304142c40e0c5eea1958b4a3ebbeb29fdbc0d4a1e9d8bde2f713ffa972d825cd80f11958f19d5bcf97503857a2692c542755ede01ff + checksum: 10c0/881db461777fe6dd6e5bca8f209a3a69312b0010317051cfc9d2f996db87072957f7acbe9d5ad6fecb17e79d68f3d1b1e89b5d45c2bbc1fd3e9183d96d5737a8 languageName: node linkType: hard @@ -8274,25 +7735,25 @@ __metadata: "@swc/core": "npm:1.11.31" "@types/bcryptjs": "npm:2.4.6" "@types/cors": "npm:2.8.19" - "@types/express": "npm:4.17.21" - "@types/express-session": "npm:1.18.1" + "@types/express": "npm:4.17.23" + "@types/express-session": "npm:1.18.2" "@types/faker": "npm:5.5.9" "@types/hash-sum": "npm:^1.0.0" "@types/js-yaml": "npm:4.0.9" "@types/lodash.groupby": "npm:4.6.9" "@types/lodash.isequal": "npm:^4.5.8" - "@types/memoizee": "npm:0.4.11" + "@types/memoizee": "npm:0.4.12" "@types/mime": "npm:4.0.0" "@types/murmurhash3js": "npm:^3.0.7" "@types/mustache": "npm:^4.2.5" "@types/node": "npm:22.15.18" "@types/nodemailer": "npm:6.4.17" "@types/owasp-password-strength-test": "npm:1.3.2" - "@types/pg": "npm:8.15.2" - "@types/semver": "npm:7.5.8" + "@types/pg": "npm:8.15.4" + "@types/semver": "npm:7.7.0" "@types/slug": "npm:^5.0.8" "@types/stoppable": "npm:1.1.3" - "@types/supertest": "npm:6.0.2" + "@types/supertest": "npm:6.0.3" "@types/type-is": "npm:1.6.7" "@types/uuid": "npm:9.0.8" "@vitest/coverage-v8": "npm:^3.1.3" @@ -8316,7 +7777,7 @@ __metadata: db-migrate-shared: "npm:1.2.0" deep-object-diff: "npm:^1.1.9" deepmerge: "npm:^4.3.1" - del-cli: "npm:5.1.0" + del-cli: "npm:6.0.0" errorhandler: "npm:^1.5.1" express: "npm:^4.21.2" express-rate-limit: "npm:^7.3.1" @@ -8375,7 +7836,7 @@ __metadata: type-is: "npm:^1.6.18" typescript: "npm:5.8.3" ulidx: "npm:^2.4.1" - unleash-client: "npm:^6.6.0" + unleash-client: "npm:^6.7.0-beta.0" uuid: "npm:^9.0.0" vite-node: "npm:^3.1.3" vitest: "npm:^3.1.3" @@ -8476,16 +7937,6 @@ __metadata: languageName: node linkType: hard -"validate-npm-package-license@npm:^3.0.1": - version: 3.0.4 - resolution: "validate-npm-package-license@npm:3.0.4" - dependencies: - spdx-correct: "npm:^3.0.0" - spdx-expression-parse: "npm:^3.0.0" - checksum: 10c0/7b91e455a8de9a0beaa9fe961e536b677da7f48c9a493edf4d4d4a87fd80a7a10267d438723364e432c2fcd00b5650b5378275cded362383ef570276e6312f4f - languageName: node - linkType: hard - "validator@npm:^13.7.0": version: 13.11.0 resolution: "validator@npm:13.11.0" @@ -8826,7 +8277,7 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^20.2.2, yargs-parser@npm:^20.2.9": +"yargs-parser@npm:^20.2.2": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9" checksum: 10c0/0685a8e58bbfb57fab6aefe03c6da904a59769bd803a722bb098bd5b0f29d274a1357762c7258fb487512811b8063fb5d2824a3415a0a4540598335b3b086c72 @@ -8896,13 +8347,6 @@ __metadata: languageName: node linkType: hard -"yocto-queue@npm:^0.1.0": - version: 0.1.0 - resolution: "yocto-queue@npm:0.1.0" - checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f - languageName: node - linkType: hard - "z-schema@npm:^5.0.1": version: 5.0.6 resolution: "z-schema@npm:5.0.6"