diff --git a/.github/workflows/playwright-tests.yml b/.github/workflows/playwright-tests.yml index 2d226af1..c9efa169 100644 --- a/.github/workflows/playwright-tests.yml +++ b/.github/workflows/playwright-tests.yml @@ -39,7 +39,7 @@ jobs: playwright-test-runner: strategy: matrix: - test_case: ['@login'] + test_case: ['@login', '@registration'] timeout-minutes: 20 runs-on: - ubuntu-22.04 diff --git a/e2e/data/reset.ts b/e2e/data/reset.ts new file mode 100644 index 00000000..736ec595 --- /dev/null +++ b/e2e/data/reset.ts @@ -0,0 +1,107 @@ +import { Client } from "pg"; + +async function getDatabaseClient() { + testEnvChecks(); + const env = getEnv(); + const client = new Client(env.superuser); + await client.connect(); + return client; +} + +function getEnv() { + return { + host: process.env.TEST_POSTGRES_HOST || "localhost", + port: process.env.TEST_POSTGRES_HOST + ? Number(process.env.TEST_POSTGRES_HOST) + : 5432, + user: process.env.TEST_POSTGRES_USER || "test_linkwarden_user", + testDatabase: process.env.TEST_POSTGRES_DATABASE || "test_linkwarden_db", + testDatabaseTemplate: + process.env.TEST_POSTGRES_DATABASE_TEMPLATE || "test_linkwarden_db_template", + productionDatabase: process.env.PRODUCTION_POSTGRES_DATABASE || "linkwarden", + superuser: { + host: process.env.PGHOST || "localhost", + port: process.env.PGPORT ? Number(process.env.PGPORT) : 5432, + user: process.env.PGUSER || "postgres", + password: process.env.PGPASSWORD || "password", + database: process.env.PGDATABASE || "postgres", + }, + }; +} + +function testEnvChecks() { + const env = getEnv(); + if (!env.testDatabase.startsWith("test_")) { + const msg = + "Please make sure your test environment database name starts with test_"; + console.error(msg); + throw new Error(msg); + } + if (env.testDatabase === env.productionDatabase) { + const msg = + "Please make sure your test environment database and production environment database names are not equal"; + console.error(msg); + throw new Error(msg); + } +} + +async function createTemplateDatabase(client: Client) { + const { user, testDatabase, testDatabaseTemplate } = getEnv(); + try { + // close current connections + await client.query(` + ALTER DATABASE ${testDatabase} WITH ALLOW_CONNECTIONS false; + SELECT pg_terminate_backend(pid) FROM pg_stat_activity + WHERE datname='${testDatabase}'; + `); + await client.query(` + CREATE DATABASE ${testDatabaseTemplate} WITH + OWNER=${user} + TEMPLATE=${testDatabase} + IS_TEMPLATE=true; + `); + } catch (e: any) { + if (e.code === "42P04") { + return; + } + throw e; + } +} + +async function createTestDatabase(client: Client) { + const { user, testDatabase, testDatabaseTemplate } = getEnv(); + const deleteDatabase = `${testDatabase}_del`; + // drop connections and alter database name + await client.query(` + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname='${testDatabase}'; + ALTER DATABASE ${testDatabase} + RENAME TO ${deleteDatabase}; + `); + await client.query(` + CREATE DATABASE ${testDatabase} + WITH OWNER ${user} + TEMPLATE=${testDatabaseTemplate}; + `); + await client.query(`DROP DATABASE ${deleteDatabase}`); +} + +export async function resetDatabase() { + const client = await getDatabaseClient(); + await createTemplateDatabase(client); + await createTestDatabase(client); + await client.end(); +} + +export async function dropTemplate() { + const client = await getDatabaseClient(); + const env = getEnv(); + try { + await client.query( + `ALTER DATABASE ${env.testDatabaseTemplate} is_template false` + ); + await client.query(`DROP DATABASE ${env.testDatabaseTemplate}`); + } catch (e) {} + await client.end(); +} \ No newline at end of file diff --git a/e2e/data/user.ts b/e2e/data/user.ts index 3ae0b73e..9ac8d9c6 100644 --- a/e2e/data/user.ts +++ b/e2e/data/user.ts @@ -5,13 +5,13 @@ axios.defaults.baseURL = "http://localhost:3000" export async function seedUser (username?: string, password?: string, name?: string) { try { return await axios.post("/api/v1/users", { - username: username || "test", - password: password || "password", + username: username || process.env["TEST_USERNAME"] || "test", + password: password || process.env["TEST_PASSWORD"] || "password", name: name || "Test User", }) } catch (e: any) { if (e instanceof AxiosError) { - if (e.response?.status === 400) { + if ([400, 500].includes(e.response?.status)) { return } } diff --git a/e2e/fixtures/index.ts b/e2e/fixtures/index.ts index a0cebe2f..0664cb93 100644 --- a/e2e/fixtures/index.ts +++ b/e2e/fixtures/index.ts @@ -2,12 +2,48 @@ import { test as baseTest } from "@playwright/test"; import { LoginPage } from "./login-page"; import { RegistrationPage } from "./registration-page"; import { DashboardPage } from "./base/dashboard-page"; +import { resetDatabase } from "../data/reset"; +import axios, { AxiosError } from "axios"; +import { signIn } from "next-auth/react"; +import { seedUser } from "../data/user"; export const test = baseTest.extend<{ + resetDatabaseFixture: void; dashboardPage: DashboardPage; loginPage: LoginPage; registrationPage: RegistrationPage; }>({ + resetDatabaseFixture: [ + async function ({ loginPage }, use) { + await resetDatabase(); + await seedUser(); + + await loginPage.goto(); + await loginPage.page.reload(); + await loginPage.usernameInput.fill("test-fake"); + await loginPage.passwordInput.fill("password-fake"); + await loginPage.submitLoginButton.click(); + await ( + await loginPage.getLatestToast() + ).locator.waitFor({ state: "visible" }); + await loginPage.page.reload(); + try { + // await axios.get("") + await signIn("credentials", { + username: process.env["TEST_USERNAME"] || "test", + password: process.env["TEST_PASSWORD"] || "password", + redirect: false, + }); + //await axios.get("http://localhost:3000/login"); + } catch (e) { + // console.log("error", e); + } + // await page.waitForTimeout(1000); + + await use(); + }, + { auto: true, timeout: 10000 }, + ], page: async ({ page }, use) => { await page.goto("/"); use(page); diff --git a/e2e/fixtures/login-page.ts b/e2e/fixtures/login-page.ts index 375da6b0..b418276d 100644 --- a/e2e/fixtures/login-page.ts +++ b/e2e/fixtures/login-page.ts @@ -23,5 +23,6 @@ export class LoginPage extends BasePage { async goto() { await this.page.goto("/login"); + await this.loginForm.waitFor({ state: "visible" }); } } diff --git a/e2e/fixtures/registration-page.ts b/e2e/fixtures/registration-page.ts index 64a407bf..2983f946 100644 --- a/e2e/fixtures/registration-page.ts +++ b/e2e/fixtures/registration-page.ts @@ -25,4 +25,9 @@ export class RegistrationPage extends BasePage { this.passwordInput = page.getByTestId("password-input"); this.usernameInput = page.getByTestId("username-input"); } + + async goto() { + await this.page.goto("/register"); + await this.registrationForm.waitFor({ state: "visible" }); + } } diff --git a/e2e/tests/global/setup.public.ts b/e2e/tests/global/setup.public.ts index 42ec8fea..cfad86cb 100644 --- a/e2e/tests/global/setup.public.ts +++ b/e2e/tests/global/setup.public.ts @@ -2,7 +2,5 @@ import { seedUser } from "@/e2e/data/user"; import { test as setup } from "../../index"; setup("Setup the default user", async () => { - const username = process.env["TEST_USERNAME"] || ""; - const password = process.env["TEST_PASSWORD"] || ""; - await seedUser(username, password); + await seedUser(); }); diff --git a/e2e/tests/global/teardown.ts b/e2e/tests/global/teardown.ts new file mode 100644 index 00000000..a21e9cf8 --- /dev/null +++ b/e2e/tests/global/teardown.ts @@ -0,0 +1,7 @@ +import { test } from "@playwright/test"; +import { dropTemplate, resetDatabase } from "../../data/reset"; + +test("Reset the database and the drop the template database", async () => { + await resetDatabase(); + await dropTemplate(); +}); diff --git a/e2e/tests/public/register.spec.ts b/e2e/tests/public/register.spec.ts new file mode 100644 index 00000000..752085a3 --- /dev/null +++ b/e2e/tests/public/register.spec.ts @@ -0,0 +1,109 @@ +import { expect, test } from "../../index"; + +test.describe( + "Registration test suite", + { + tag: "@registration", + }, + async () => { + test("Registration page is accessible from login page", async ({ + loginPage, + registrationPage, + }) => { + await loginPage.goto(); + await loginPage.registerLink.click(); + await expect(registrationPage.registrationForm).toBeVisible(); + }); + + test("Login page is accessible from registration page", async ({ + loginPage, + registrationPage, + }) => { + await registrationPage.goto(); + await registrationPage.loginLink.click(); + await expect(loginPage.loginForm).toBeVisible(); + }); + + test("Ensure filling out all the fields is required and registration works after errors", async ({ + loginPage, + registrationPage, + }) => { + await registrationPage.goto(); + await test.step("An empty form gives an error", async () => { + await registrationPage.registerButton.click(); + const toast = await registrationPage.getLatestToast(); + await expect(toast.locator).toHaveAttribute("data-type", "error"); + await toast.closeButton.click(); + await toast.locator.waitFor({ state: "hidden" }); + }); + await test.step("Filling in only the display name gives an error", async () => { + await registrationPage.displayNameInput.fill("Display name"); + await registrationPage.registerButton.click(); + const toast = await registrationPage.getLatestToast(); + await expect(toast.locator).toHaveAttribute("data-type", "error"); + await toast.closeButton.click(); + await toast.locator.waitFor({ state: "hidden" }); + }); + await test.step("Filling in only the display name and username gives and error", async () => { + await registrationPage.usernameInput.fill("testing-username-123"); + await registrationPage.registerButton.click(); + const toast = await registrationPage.getLatestToast(); + await expect(toast.locator).toHaveAttribute("data-type", "error"); + await toast.closeButton.click(); + await toast.locator.waitFor({ state: "hidden" }); + }); + await test.step("Filling in only the display name, username, and password gives and error", async () => { + await registrationPage.passwordInput.fill("pP24^%$boau"); + await registrationPage.registerButton.click(); + const toast = await registrationPage.getLatestToast(); + await expect(toast.locator).toHaveAttribute("data-type", "error"); + await toast.closeButton.click(); + await toast.locator.waitFor({ state: "hidden" }); + }); + await test.step("Ensure after filling in the form correctly, it still let's the user register", async () => { + await registrationPage.passwordConfirmInput.fill("pP24^%$boau"); + await registrationPage.registerButton.click(); + await expect(loginPage.loginForm).toBeVisible(); + const toast = await registrationPage.getLatestToast(); + await expect(toast.locator).toHaveAttribute("data-type", "success"); + }); + }); + + test("After successful registration user can successfully login", async ({ + dashboardPage, + loginPage, + registrationPage, + }) => { + await test.step("Register the user", async () => { + await registrationPage.goto(); + await registrationPage.displayNameInput.fill("Test name"); + await registrationPage.usernameInput.fill("new-registration-username"); + await registrationPage.passwordInput.fill("Pp4seword@1"); + await registrationPage.passwordConfirmInput.fill("Pp4seword@1"); + await registrationPage.registerButton.click(); + await expect(loginPage.loginForm).toBeVisible(); + const toast = await registrationPage.getLatestToast(); + await expect(toast.locator).toHaveAttribute("data-type", "success"); + }); + await test.step("Login the user", async () => { + await loginPage.usernameInput.fill("new-registration-username"); + await loginPage.passwordInput.fill("Pp4seword@1"); + await loginPage.submitLoginButton.click(); + await expect(dashboardPage.container).toBeVisible(); + }); + }); + + test("Ensure mismatching passwords gives an error", async ({ + registrationPage, + }) => { + await registrationPage.goto(); + await registrationPage.displayNameInput.fill("Test name"); + await registrationPage.usernameInput.fill("new-test-username"); + await registrationPage.passwordInput.fill("Pp4seword@1"); + await registrationPage.passwordConfirmInput.fill("Pp4seword@1333"); + await registrationPage.registerButton.click(); + const toast = await registrationPage.getLatestToast(); + await expect(toast.locator).toHaveAttribute("data-type", "error"); + }); + } +); diff --git a/package.json b/package.json index a41987e4..b549fe37 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "next-auth": "^4.22.1", "node-fetch": "^2.7.0", "nodemailer": "^6.9.3", + "pg": "^8.11.5", "playwright": "^1.43.1", "react": "18.2.0", "react-colorful": "^5.6.1", @@ -75,6 +76,7 @@ "@types/dompurify": "^3.0.4", "@types/jsdom": "^21.1.3", "@types/node-fetch": "^2.6.10", + "@types/pg": "^8.11.5", "@types/shelljs": "^0.8.15", "autoprefixer": "^10.4.14", "daisyui": "^4.4.2", diff --git a/playwright.config.ts b/playwright.config.ts index b90f7d74..4156f910 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -15,9 +15,10 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + workers: 1, // process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: "html", + expect: { timeout: 15000 }, /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ @@ -32,10 +33,16 @@ export default defineConfig({ { name: "setup dashboard", testMatch: /global\/setup\.dashboard\.ts/, + teardown: "cleanup test database", }, { name: "setup public", testMatch: /global\/setup\.public\.ts/, + teardown: "cleanup test database", + }, + { + name: "cleanup test database", + testMatch: /global\/teardown\.ts/, }, { name: "chromium dashboard", diff --git a/yarn.lock b/yarn.lock index ece2559f..6a289f49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2017,6 +2017,15 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/pg@^8.11.5": + version "8.11.5" + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.11.5.tgz#a1ffb4dc4a46a83bda096cb298051a5b171de167" + integrity sha512-2xMjVviMxneZHDHX5p5S6tsRRs7TpDHeeK7kTTMe/kAC/mRRNjWHjZg0rkiY+e17jXSZV3zJYDxXV8Cy72/Vuw== + dependencies: + "@types/node" "*" + pg-protocol "*" + pg-types "^4.0.1" + "@types/prop-types@*": version "15.7.5" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" @@ -4729,6 +4738,11 @@ object.values@^1.1.6: define-properties "^1.1.4" es-abstract "^1.20.4" +obuf@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + oidc-token-hash@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz#9a229f0a1ce9d4fc89bcaee5478c97a889e7b7b6" @@ -4885,6 +4899,80 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== +pg-cloudflare@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" + integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== + +pg-connection-string@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.4.tgz#f543862adfa49fa4e14bc8a8892d2a84d754246d" + integrity sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA== + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-numeric@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a" + integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw== + +pg-pool@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.2.tgz#3a592370b8ae3f02a7c8130d245bc02fa2c5f3f2" + integrity sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg== + +pg-protocol@*, pg-protocol@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.1.tgz#21333e6d83b01faaebfe7a33a7ad6bfd9ed38cb3" + integrity sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg== + +pg-types@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg-types@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-4.0.2.tgz#399209a57c326f162461faa870145bb0f918b76d" + integrity sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng== + dependencies: + pg-int8 "1.0.1" + pg-numeric "1.0.2" + postgres-array "~3.0.1" + postgres-bytea "~3.0.0" + postgres-date "~2.1.0" + postgres-interval "^3.0.0" + postgres-range "^1.1.1" + +pg@^8.11.5: + version "8.11.5" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.5.tgz#e722b0a5f1ed92931c31758ebec3ddf878dd4128" + integrity sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw== + dependencies: + pg-connection-string "^2.6.4" + pg-pool "^3.6.2" + pg-protocol "^1.6.1" + pg-types "^2.1.0" + pgpass "1.x" + optionalDependencies: + pg-cloudflare "^1.1.1" + +pgpass@1.x: + version "1.0.5" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== + dependencies: + split2 "^4.1.0" + phin@^2.9.1: version "2.9.3" resolved "https://registry.yarnpkg.com/phin/-/phin-2.9.3.tgz#f9b6ac10a035636fb65dfc576aaaa17b8743125c" @@ -5003,6 +5091,55 @@ postcss@^8.4.23, postcss@^8.4.26: picocolors "^1.0.0" source-map-js "^1.0.2" +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-array@~3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-3.0.2.tgz#68d6182cb0f7f152a7e60dc6a6889ed74b0a5f98" + integrity sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== + +postgres-bytea@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-3.0.0.tgz#9048dc461ac7ba70a6a42d109221619ecd1cb089" + integrity sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw== + dependencies: + obuf "~1.1.2" + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-date@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-2.1.0.tgz#b85d3c1fb6fb3c6c8db1e9942a13a3bf625189d0" + integrity sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +postgres-interval@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-3.0.0.tgz#baf7a8b3ebab19b7f38f07566c7aab0962f0c86a" + integrity sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw== + +postgres-range@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863" + integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w== + preact-render-to-string@5.2.3: version "5.2.3" resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz#23d17376182af720b1060d5a4099843c7fe92fe4" @@ -5537,6 +5674,11 @@ spawn-command@0.0.2: resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e" integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ== +split2@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + sprintf-js@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a"