merge: main -> resolution-flow-celebration
This commit is contained in:
Коммит
e3e88474d6
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
32
package.json
32
package.json
|
@ -46,21 +46,21 @@
|
|||
"npm": "9.6.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.450.0",
|
||||
"@aws-sdk/lib-storage": "^3.450.0",
|
||||
"@aws-sdk/client-s3": "^3.454.0",
|
||||
"@aws-sdk/lib-storage": "^3.454.0",
|
||||
"@fluent/bundle": "^0.18.0",
|
||||
"@fluent/langneg": "^0.7.0",
|
||||
"@fluent/react": "^0.15.2",
|
||||
"@google-cloud/logging-winston": "^6.0.0",
|
||||
"@google-cloud/pubsub": "^4.0.6",
|
||||
"@google-cloud/pubsub": "^4.0.7",
|
||||
"@grpc/grpc-js": "1.9.7",
|
||||
"@leeoniya/ufuzzy": "^1.0.11",
|
||||
"@mozilla/glean": "2.0.5",
|
||||
"@sentry/nextjs": "^7.80.0",
|
||||
"@sentry/nextjs": "^7.80.1",
|
||||
"@sentry/node": "^7.58.1",
|
||||
"@sentry/tracing": "^7.80.0",
|
||||
"@types/jsdom": "^21.1.4",
|
||||
"@types/node": "^20.9.0",
|
||||
"@sentry/tracing": "^7.80.1",
|
||||
"@types/jsdom": "^21.1.5",
|
||||
"@types/node": "^20.9.1",
|
||||
"@types/react": "^18.2.31",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"canvas-confetti": "^1.9.1",
|
||||
|
@ -77,15 +77,15 @@
|
|||
"patch-package": "^8.0.0",
|
||||
"pg": "^8.11.3",
|
||||
"react": "^18.2.0",
|
||||
"react-aria": "^3.29.1",
|
||||
"react-aria": "^3.30.0",
|
||||
"react-cookie": "^6.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-stately": "^3.27.1",
|
||||
"react-stately": "^3.28.0",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.2.0",
|
||||
"@faker-js/faker": "^8.3.1",
|
||||
"@playwright/test": "^1.36.1",
|
||||
"@storybook/addon-a11y": "^7.5.3",
|
||||
"@storybook/addon-essentials": "^7.5.3",
|
||||
|
@ -96,20 +96,20 @@
|
|||
"@storybook/react": "^7.5.1",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
"@testing-library/jest-dom": "^6.1.4",
|
||||
"@testing-library/react": "^14.1.0",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"@types/adm-zip": "^0.5.3",
|
||||
"@types/adm-zip": "^0.5.4",
|
||||
"@types/canvas-confetti": "^1.6.3",
|
||||
"@types/jest-axe": "^3.5.7",
|
||||
"@types/jsonwebtoken": "^9.0.4",
|
||||
"@types/jwk-to-pem": "^2.0.2",
|
||||
"@types/nodemailer": "^6.4.13",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/uuid": "^9.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"adm-zip": "^0.5.10",
|
||||
"c8": "^8.0.1",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-check-file": "^2.6.2",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
|
@ -123,14 +123,14 @@
|
|||
"jest-canvas-mock": "^2.5.2",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-fail-on-console": "^3.1.1",
|
||||
"lint-staged": "^15.0.2",
|
||||
"lint-staged": "^15.1.0",
|
||||
"prettier": "3.0.3",
|
||||
"react-intersection-observer": "^9.5.2",
|
||||
"sass": "^1.69.4",
|
||||
"storybook": "^7.5.3",
|
||||
"stylelint": "^15.11.0",
|
||||
"stylelint-config-recommended-scss": "^13.1.0",
|
||||
"stylelint-scss": "^5.2.1",
|
||||
"stylelint-scss": "^5.3.1",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2"
|
||||
|
|
|
@ -13,6 +13,7 @@ import { WelcomeToPremiumView } from "./WelcomeToPremiumView";
|
|||
import { getSubscriberEmails } from "../../../../../../../../functions/server/getSubscriberEmails";
|
||||
import { StepDeterminationData } from "../../../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
import { getCountryCode } from "../../../../../../../../functions/server/getCountryCode";
|
||||
import { activateAndOptoutProfile } from "../../../../../../../../functions/server/onerep";
|
||||
|
||||
export default async function WelcomeToPremiumPage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
@ -35,6 +36,12 @@ export default async function WelcomeToPremiumPage() {
|
|||
user: session.user,
|
||||
};
|
||||
|
||||
// If the current user is a subscriber and their OneRep profile is not
|
||||
// activated: Most likely we were not able or failed to kick-off the
|
||||
// auto-removal process.
|
||||
// Let’s make sure the users OneRep profile is activated:
|
||||
await activateAndOptoutProfile(profileId);
|
||||
|
||||
return (
|
||||
<WelcomeToPremiumView data={data} subscriberEmails={subscriberEmails} />
|
||||
);
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
import { getOnerepProfileId } from "../../../../../../db/tables/subscribers";
|
||||
|
||||
import {
|
||||
activateAndOptoutProfile,
|
||||
isEligibleForFreeScan,
|
||||
isEligibleForPremium,
|
||||
} from "../../../../../functions/server/onerep";
|
||||
|
@ -57,10 +58,11 @@ export default async function DashboardPage() {
|
|||
const isNewUser =
|
||||
(parseIso8601Datetime(session.user.subscriber.created_at)?.getTime() ?? 0) >
|
||||
brokerScanReleaseDate.getTime();
|
||||
const isPremiumUser = hasPremium(session.user);
|
||||
|
||||
if (
|
||||
!hasRunScan &&
|
||||
(hasPremium(session.user) ||
|
||||
(isPremiumUser ||
|
||||
(isNewUser &&
|
||||
canSubscribeToPremium({
|
||||
user: session.user,
|
||||
|
@ -72,6 +74,14 @@ export default async function DashboardPage() {
|
|||
|
||||
await refreshStoredScanResults(profileId);
|
||||
|
||||
// If the current user is a subscriber and their OneRep profile is not
|
||||
// activated: Most likely we were not able or failed to kick-off the
|
||||
// auto-removal process.
|
||||
// Let’s make sure the users OneRep profile is activated:
|
||||
if (isPremiumUser) {
|
||||
await activateAndOptoutProfile(profileId);
|
||||
}
|
||||
|
||||
const latestScan = await getLatestOnerepScanResults(profileId);
|
||||
const scanCount = await getScansCountForProfile(profileId);
|
||||
const subBreaches = await getSubscriberBreaches(session.user);
|
||||
|
|
|
@ -94,9 +94,6 @@ export const EnterInfo = ({
|
|||
),
|
||||
value: firstName,
|
||||
displayValue: firstName,
|
||||
errorMessage: l10n.getString(
|
||||
"onboarding-enter-details-input-error-message-generic",
|
||||
),
|
||||
isValid: firstName.trim() !== "",
|
||||
onChange: setFirstName,
|
||||
},
|
||||
|
@ -109,9 +106,6 @@ export const EnterInfo = ({
|
|||
),
|
||||
value: lastName,
|
||||
displayValue: lastName,
|
||||
errorMessage: l10n.getString(
|
||||
"onboarding-enter-details-input-error-message-generic",
|
||||
),
|
||||
isValid: lastName.trim() !== "",
|
||||
onChange: setLastName,
|
||||
},
|
||||
|
@ -124,9 +118,6 @@ export const EnterInfo = ({
|
|||
),
|
||||
value: location,
|
||||
displayValue: location,
|
||||
errorMessage: l10n.getString(
|
||||
"onboarding-enter-details-input-error-message-location",
|
||||
),
|
||||
isValid: location.trim() !== "",
|
||||
onChange: setLocation,
|
||||
},
|
||||
|
@ -140,9 +131,6 @@ export const EnterInfo = ({
|
|||
dateStyle: "medium",
|
||||
timeZone: "UTC",
|
||||
}),
|
||||
errorMessage: l10n.getString(
|
||||
"onboarding-enter-details-input-error-message-generic",
|
||||
),
|
||||
isValid: meetsAgeRequirement(dateOfBirth),
|
||||
onChange: setDateOfBirth,
|
||||
},
|
||||
|
@ -326,34 +314,28 @@ export const EnterInfo = ({
|
|||
<form onSubmit={handleOnSubmit}>
|
||||
<div className={styles.inputContainer}>
|
||||
{userDetailsData.map(
|
||||
({
|
||||
key,
|
||||
errorMessage,
|
||||
label,
|
||||
onChange,
|
||||
placeholder,
|
||||
isValid,
|
||||
type,
|
||||
value,
|
||||
}) => {
|
||||
({ key, label, onChange, placeholder, isValid, type, value }) => {
|
||||
const validationState =
|
||||
!isValid && invalidInputs.includes(key) ? "invalid" : "valid";
|
||||
return key === "location" ? (
|
||||
<LocationAutocompleteInput
|
||||
key={key}
|
||||
errorMessage={errorMessage}
|
||||
errorMessage={l10n.getString(
|
||||
"onboarding-enter-details-input-error-message-location",
|
||||
)}
|
||||
label={label}
|
||||
isRequired={true}
|
||||
onChange={onChange}
|
||||
onInputChange={onChange}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
validationState={validationState}
|
||||
value={value}
|
||||
inputValue={value}
|
||||
/>
|
||||
) : (
|
||||
<InputField
|
||||
key={key}
|
||||
errorMessage={errorMessage}
|
||||
errorMessage={l10n.getString(
|
||||
"onboarding-enter-details-input-error-message-generic",
|
||||
)}
|
||||
label={label}
|
||||
isRequired={true}
|
||||
onChange={onChange}
|
||||
|
|
|
@ -12,9 +12,8 @@ import {
|
|||
getSubscribersByHashes,
|
||||
} from "../../../../../../db/tables/subscribers";
|
||||
import {
|
||||
activateProfile,
|
||||
activateAndOptoutProfile,
|
||||
deactivateProfile,
|
||||
optoutProfile,
|
||||
} from "../../../../../functions/server/onerep";
|
||||
import { captureException } from "@sentry/node";
|
||||
import { deleteProfileDetails } from "../../../../../../db/tables/onerep_profiles";
|
||||
|
@ -120,8 +119,7 @@ export async function PUT(
|
|||
switch (action) {
|
||||
case "subscribe": {
|
||||
// activate and opt out profiles
|
||||
await activateProfile(onerepProfileId);
|
||||
await optoutProfile(onerepProfileId);
|
||||
await activateAndOptoutProfile(onerepProfileId);
|
||||
logger.info("force_user_subscribe", {
|
||||
onerepProfileId,
|
||||
primarySha1,
|
||||
|
|
|
@ -16,23 +16,27 @@ interface ComboBoxProps extends ComboBoxStateOptions<object> {
|
|||
}
|
||||
|
||||
function ComboBox(props: ComboBoxProps) {
|
||||
const { errorMessage, label, isRequired, validationState } = props;
|
||||
const { label, isRequired, validationState } = props;
|
||||
const inputRef = useRef(null);
|
||||
const listBoxRef = useRef(null);
|
||||
const popoverRef = useRef(null);
|
||||
const state = useComboBoxState({ ...props });
|
||||
const { inputProps, listBoxProps, labelProps, errorMessageProps } =
|
||||
useComboBox(
|
||||
{
|
||||
...props,
|
||||
inputRef,
|
||||
listBoxRef,
|
||||
popoverRef,
|
||||
},
|
||||
state,
|
||||
);
|
||||
const {
|
||||
inputProps,
|
||||
listBoxProps,
|
||||
labelProps,
|
||||
errorMessageProps,
|
||||
validationErrors,
|
||||
} = useComboBox(
|
||||
{
|
||||
...props,
|
||||
inputRef,
|
||||
listBoxRef,
|
||||
popoverRef,
|
||||
},
|
||||
state,
|
||||
);
|
||||
const isInvalid = validationState === "invalid";
|
||||
const showError = errorMessage && isInvalid;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -54,9 +58,16 @@ function ComboBox(props: ComboBoxProps) {
|
|||
!inputProps.value ? styles.noValue : ""
|
||||
} ${isInvalid ? styles.hasError : ""}`}
|
||||
/>
|
||||
{showError && (
|
||||
{isInvalid && (
|
||||
<div {...errorMessageProps} className={styles.inputMessage}>
|
||||
{errorMessage}
|
||||
{
|
||||
// We always pass in a string at the time of writing, so we can't
|
||||
// hit the "else" path with tests:
|
||||
/* c8 ignore next 3 */
|
||||
typeof props.errorMessage === "string"
|
||||
? props.errorMessage
|
||||
: validationErrors.join(" ")
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -9,14 +9,11 @@ import { AriaTextFieldProps, useTextField } from "react-aria";
|
|||
import styles from "./InputField.module.scss";
|
||||
|
||||
export const InputField = (props: AriaTextFieldProps) => {
|
||||
const { errorMessage, isRequired, label, validationState, value } = props;
|
||||
const { isRequired, label, validationState, value } = props;
|
||||
const inputRef = useRef(null);
|
||||
const { errorMessageProps, inputProps, labelProps } = useTextField(
|
||||
props,
|
||||
inputRef,
|
||||
);
|
||||
const { errorMessageProps, validationErrors, inputProps, labelProps } =
|
||||
useTextField(props, inputRef);
|
||||
const isInvalid = validationState === "invalid";
|
||||
const showError = errorMessage && isInvalid;
|
||||
|
||||
return (
|
||||
<div className={styles.input}>
|
||||
|
@ -35,9 +32,16 @@ export const InputField = (props: AriaTextFieldProps) => {
|
|||
isInvalid ? styles.hasError : ""
|
||||
}`}
|
||||
/>
|
||||
{showError && (
|
||||
{isInvalid && (
|
||||
<div {...errorMessageProps} className={styles.inputMessage}>
|
||||
{errorMessage}
|
||||
{
|
||||
// We always pass in a string at the time of writing, so we can't
|
||||
// hit the "else" path with tests:
|
||||
/* c8 ignore next 3 */
|
||||
typeof props.errorMessage === "string"
|
||||
? props.errorMessage
|
||||
: validationErrors.join(" ")
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
"use client";
|
||||
|
||||
import { Key, ReactNode, RefObject, useRef } from "react";
|
||||
import { ReactNode, RefObject, useRef } from "react";
|
||||
import { AriaListBoxOptions, useListBox, useOption } from "react-aria";
|
||||
import { ListState } from "react-stately";
|
||||
import { useElementWidth } from "../../hooks/useElementWidth";
|
||||
|
@ -12,7 +12,7 @@ import styles from "./ListBox.module.scss";
|
|||
|
||||
export interface OptionProps extends AriaListBoxOptions<unknown> {
|
||||
item: {
|
||||
key: Key;
|
||||
key: Parameters<typeof useOption>[0]["key"];
|
||||
rendered: ReactNode;
|
||||
};
|
||||
state: ListState<object>;
|
||||
|
|
|
@ -4,9 +4,8 @@
|
|||
|
||||
"use client";
|
||||
|
||||
import { Key, useDeferredValue, useEffect, useState } from "react";
|
||||
import { AriaTextFieldProps } from "react-aria";
|
||||
import { Item } from "react-stately";
|
||||
import { useDeferredValue, useEffect, useState } from "react";
|
||||
import { ComboBoxStateOptions, Item } from "react-stately";
|
||||
import { ComboBox } from "./ComboBox";
|
||||
import {
|
||||
MatchingLocations,
|
||||
|
@ -61,20 +60,26 @@ function getLocationString(location: RelevantLocation) {
|
|||
return `${name}, ${stateCode}, ${countryCode}`;
|
||||
}
|
||||
|
||||
function getLocationStringByKey(locations: Array<RelevantLocation>, key: Key) {
|
||||
function getLocationStringByKey(
|
||||
locations: Array<RelevantLocation>,
|
||||
key: ComboBoxStateOptions<object>["selectedKey"],
|
||||
) {
|
||||
const location = locations.find(({ id }) => id === key);
|
||||
// TODO: Add unit test when changing this code:
|
||||
/* c8 ignore next */
|
||||
return location ? getLocationString(location) : "";
|
||||
}
|
||||
|
||||
export const LocationAutocompleteInput = (props: AriaTextFieldProps) => {
|
||||
export const LocationAutocompleteInput = (
|
||||
props: ComboBoxStateOptions<object>,
|
||||
) => {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const deferredSearchQuery = useDeferredValue(searchQuery);
|
||||
|
||||
const [locationSuggestions, setLocationSuggestions] =
|
||||
useState<MatchingLocations>([]);
|
||||
const [selectedKey, setSelectedKey] = useState<Key>("");
|
||||
const [selectedKey, setSelectedKey] =
|
||||
useState<ComboBoxStateOptions<object>["selectedKey"]>("");
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
|
@ -137,12 +142,14 @@ export const LocationAutocompleteInput = (props: AriaTextFieldProps) => {
|
|||
}
|
||||
|
||||
setSearchQuery(inputValue);
|
||||
props.onChange?.(inputValue);
|
||||
props.onInputChange?.(inputValue);
|
||||
};
|
||||
|
||||
// TODO: Add unit test when changing this code:
|
||||
/* c8 ignore next 3 */
|
||||
const handleOnSelectionChange = (key: Key) => {
|
||||
/* c8 ignore next 5 */
|
||||
const handleOnSelectionChange = (
|
||||
key: ComboBoxStateOptions<object>["selectedKey"],
|
||||
) => {
|
||||
setSelectedKey(key);
|
||||
};
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ export type TabListProps = TabsProps & {
|
|||
|
||||
export interface TabParams {
|
||||
item: {
|
||||
key: Key;
|
||||
key: Parameters<typeof useTab>[0]["key"];
|
||||
rendered: ReactNode;
|
||||
};
|
||||
state: TabListState<object>;
|
||||
|
|
|
@ -9,7 +9,10 @@ import {
|
|||
ISO8601DateString,
|
||||
} from "../../../utils/parse.js";
|
||||
import { StateAbbr } from "../../../utils/states.js";
|
||||
import { getLatestOnerepScanResults } from "../../../db/tables/onerep_scans";
|
||||
import {
|
||||
getAllScansForProfile,
|
||||
getLatestOnerepScanResults,
|
||||
} from "../../../db/tables/onerep_scans";
|
||||
import { RemovalStatus } from "../universal/scanResult.js";
|
||||
import {
|
||||
FeatureFlagName,
|
||||
|
@ -216,6 +219,30 @@ export async function optoutProfile(profileId: number): Promise<void> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function activateAndOptoutProfile(
|
||||
profileId: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const scans = await getAllScansForProfile(profileId);
|
||||
const hasInitialScan = scans.some(
|
||||
(scan) => scan.onerep_scan_reason === "initial",
|
||||
);
|
||||
if (hasInitialScan) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { status: profileStatus } = await getProfile(profileId);
|
||||
if (profileStatus === "inactive") {
|
||||
await activateProfile(profileId);
|
||||
}
|
||||
|
||||
await optoutProfile(profileId);
|
||||
} catch (error) {
|
||||
logger.error("Failed to activate and optout profile:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createScan(
|
||||
profileId: number,
|
||||
): Promise<CreateScanResponse> {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
* with the goal of deprecating the column
|
||||
*/
|
||||
|
||||
import { createDbConnection } from "../connect";
|
||||
import { createDbConnection } from "../db/connect.js";
|
||||
import { getAllBreachesFromDb } from "../utils/hibp.js";
|
||||
import { getAllEmailsAndBreaches } from "../utils/breaches.js";
|
||||
import { setBreachResolution } from "../db/tables/subscribers.js";
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
* with the goal of deprecating the column
|
||||
*/
|
||||
|
||||
import { createDbConnection } from "../connect";
|
||||
import { createDbConnection } from "../../db/connect.js";
|
||||
import { getAllBreachesFromDb } from "../../utils/hibp.js";
|
||||
import { getAllEmailsAndBreaches } from "../../utils/breaches.js";
|
||||
import { BreachDataTypes } from "../../utils/breach-resolution.js";
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
* `useBreachId: true/false`
|
||||
*/
|
||||
|
||||
import { createDbConnection } from "../connect";
|
||||
import { createDbConnection } from "../../db/connect.js";
|
||||
import { getAllBreachesFromDb } from "../../utils/hibp.js";
|
||||
import { getAllEmailsAndBreaches } from "../../utils/breaches.js";
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* The purpose of the script is to clean up some of the failed records during db migration on 3/28/23
|
||||
*/
|
||||
|
||||
import { createDbConnection } from "../connect";
|
||||
import { createDbConnection } from "../db/connect.js";
|
||||
import { getAllBreachesFromDb } from "../utils/hibp.js";
|
||||
import { getAllEmailsAndBreaches } from "../utils/breaches.js";
|
||||
import { setBreachResolution } from "../db/tables/subscribers.js";
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
* with the goal of deprecating the column
|
||||
*/
|
||||
|
||||
import { createDbConnection } from "../connect";
|
||||
import { createDbConnection } from "../../db/connect.js";
|
||||
import { getAllBreachesFromDb } from "../../utils/hibp.js";
|
||||
import { getAllEmailsAndBreaches } from "../../utils/breaches.js";
|
||||
import { BreachDataTypes } from "../../utils/breach-resolution.js";
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
* with the goal of deprecating the column
|
||||
*/
|
||||
|
||||
import { createDbConnection } from "../connect";
|
||||
import { createDbConnection } from "../../db/connect.js";
|
||||
import { getAllBreachesFromDb } from "../../utils/hibp.js";
|
||||
import { getAllEmailsAndBreaches } from "../../utils/breaches.js";
|
||||
import { BreachDataTypes } from "../../utils/breach-resolution.js";
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* The purpose of the script is to benchmark pure read with limit set as 100
|
||||
*/
|
||||
|
||||
import { createDbConnection } from "../connect";
|
||||
import { createDbConnection } from "../../db/connect.js";
|
||||
import { getAllBreachesFromDb } from "../../utils/hibp.js";
|
||||
|
||||
const knex = createDbConnection();
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* The purpose of the script is to benchmark pure read with limit set as 1000
|
||||
*/
|
||||
|
||||
import { createDbConnection } from "../connect";
|
||||
import { createDbConnection } from "../../db/connect.js";
|
||||
import { getAllBreachesFromDb } from "../../utils/hibp.js";
|
||||
|
||||
const knex = createDbConnection();
|
||||
|
|
Загрузка…
Ссылка в новой задаче